/* global window */
window.Niwa = window.Niwa || {};

window.Niwa.AdminChatStore = (function () {
  'use strict';

  var persistence = window.Niwa.AdminChatPersistence || null;
  var state = {
    messages: [],
    oldestTs: 0,
    newestTs: 0,
	oldestId: '',
	newestId: '',
    hasMore: false,
    pending: null,
    jobs: [],
    isOpen: false
  };
  var listeners = {};
	var persistenceSubscribed = false;

  function on(eventName, callback) {
    if (typeof callback !== 'function') return;
    listeners[eventName] = listeners[eventName] || [];
    listeners[eventName].push(callback);
  }

  function emit(eventName, payload) {
    var list = listeners[eventName] || [];
    for (var i = 0; i < list.length; i++) {
      try {
        list[i](payload || {}, getState());
      } catch (e) { /* isolate listeners */ }
    }
  }

  function getState() {
    return {
      messages: state.messages.slice(),
      oldestTs: state.oldestTs,
      newestTs: state.newestTs,
	  oldestId: state.oldestId,
	  newestId: state.newestId,
      hasMore: state.hasMore,
      pending: state.pending ? Object.assign({}, state.pending) : null,
      jobs: state.jobs.map(function (job) { return Object.assign({}, job); }),
      isOpen: state.isOpen
    };
  }

  function normalizeMessage(item) {
    if (!item || typeof item !== 'object') return null;
    var text = item.text || item.content || '';
    if (!text) return null;

    var ts = parseInt(item.ts || item.timestamp || 0, 10);
    if (!ts || ts < 0) ts = Math.floor(Date.now() / 1000);

    var role = item.role === 'user' ? 'user' : 'bot';
    var source = item.source || null;
    return {
      id: item.id || makeMessageId(role, text, ts),
      role: role,
      text: String(text),
      ts: ts,
      source: source,
	  requestId: item.request_id || item.requestId || '',
	  jobId: item.job_id || item.jobId || '',
      local: !!item.local
    };
  }

  function makeMessageId(role, text, ts) {
    var hash = 0;
    var raw = role + '|' + ts + '|' + String(text || '');
    for (var i = 0; i < raw.length; i++) {
      hash = ((hash << 5) - hash) + raw.charCodeAt(i);
      hash |= 0;
    }
    return 'admin_msg_' + ts + '_' + Math.abs(hash);
  }

  function normalizeList(items) {
    var out = [];
    var seen = {};
    var raw = Array.isArray(items) ? items : [];

    for (var i = 0; i < raw.length; i++) {
      var msg = normalizeMessage(raw[i]);
      if (!msg || seen[msg.id]) continue;
      seen[msg.id] = true;
      out.push(msg);
    }

    out.sort(function (a, b) {
      if (a.ts === b.ts) return 0;
      return a.ts < b.ts ? -1 : 1;
    });
    return out;
  }

  function isOptimisticEcho(existing, incoming) {
    if (!existing || !incoming) return false;
    if (existing.id === incoming.id) return true;
	if (!!existing.local === !!incoming.local) return false;
    if (existing.role !== incoming.role) return false;
	if (existing.requestId && incoming.requestId) return existing.requestId === incoming.requestId;
	if (existing.jobId && incoming.jobId) return existing.jobId === incoming.jobId;
    if (normalizeTextKey(existing.text) !== normalizeTextKey(incoming.text)) return false;
    if (Math.abs((existing.ts || 0) - (incoming.ts || 0)) > 5) return false;
    return existing.source === 'admin_ui' || incoming.source === 'admin_ui';
  }

  function normalizeTextKey(text) {
    return String(text || '').replace(/\s+/g, ' ').trim();
  }

  function hasMessageInList(list, message) {
    for (var i = 0; i < list.length; i++) {
      if (list[i].id === message.id || isOptimisticEcho(list[i], message)) return true;
    }
    return false;
  }

  function hasExistingMessage(message) {
    for (var i = 0; i < state.messages.length; i++) {
      if (state.messages[i].id === message.id || isOptimisticEcho(state.messages[i], message)) return true;
    }
    return false;
  }

  function recalcEdges() {
    state.oldestTs = state.messages.length ? state.messages[0].ts : 0;
    state.newestTs = state.messages.length ? state.messages[state.messages.length - 1].ts : 0;
	state.oldestId = state.messages.length ? state.messages[0].id : '';
	state.newestId = state.messages.length ? state.messages[state.messages.length - 1].id : '';
  }

  function replace(items, meta) {
    var serverMessages = normalizeList(items);
    var localMessages = pruneLocalMessagesAgainst(serverMessages);
    state.messages = normalizeList(serverMessages.concat(localMessages));
    state.hasMore = !!(meta && meta.hasMore);
    state.oldestTs = meta && meta.oldestTs ? parseInt(meta.oldestTs, 10) || 0 : 0;
    state.newestTs = meta && meta.newestTs ? parseInt(meta.newestTs, 10) || 0 : 0;
	state.oldestId = meta && meta.oldestId ? String(meta.oldestId) : '';
	state.newestId = meta && meta.newestId ? String(meta.newestId) : '';
	if (!state.oldestTs || !state.newestTs || !state.oldestId || !state.newestId) recalcEdges();
    emit('messages:replaced', { messages: state.messages.slice() });
  }

  function prepend(items, meta) {
    var incoming = normalizeList(items);
    if (!incoming.length) {
      state.hasMore = !!(meta && meta.hasMore);
      emit('messages:older-empty', {});
      return 0;
    }

    var seen = {};
    for (var i = 0; i < state.messages.length; i++) {
      seen[state.messages[i].id] = true;
    }

    var additions = [];
    for (var j = 0; j < incoming.length; j++) {
      if (seen[incoming[j].id] || hasExistingMessage(incoming[j])) continue;
      additions.push(incoming[j]);
      seen[incoming[j].id] = true;
    }

    if (!additions.length) {
      state.hasMore = !!(meta && meta.hasMore);
      return 0;
    }

    state.messages = additions.concat(state.messages);
    state.hasMore = !!(meta && meta.hasMore);
    recalcEdges();
    emit('messages:prepended', { messages: additions, count: additions.length });
    return additions.length;
  }

  function append(item) {
    var msg = normalizeMessage(item);
    if (!msg) return null;

    if (hasExistingMessage(msg)) return null;

    state.messages.push(msg);
    persistLocalMessage(msg);
    recalcEdges();
    emit('messages:appended', { message: msg });
    return msg;
  }

  function appendMany(items) {
    var incoming = normalizeList(items);
    var count = 0;
    for (var i = 0; i < incoming.length; i++) {
      if (append(incoming[i])) count++;
    }
    return count;
  }

  function setPending(text, sentAt, meta) {
    meta = meta || {};
    var pending = {
      text: String(text || ''),
      sentAt: sentAt || Date.now(),
      jobId: meta.jobId ? String(meta.jobId) : '',
      requestId: meta.requestId ? String(meta.requestId) : '',
	  session: meta.session ? String(meta.session) : '',
	  generation: parseInt(meta.generation || 0, 10) || 0,
	  sequence: parseInt(meta.sequence || 0, 10) || 0,
	  phase: meta.phase ? String(meta.phase) : 'pending'
    };
	var replaced = false;
	for (var i = 0; i < state.jobs.length; i++) {
	  if ((pending.requestId && state.jobs[i].requestId === pending.requestId) || (pending.jobId && state.jobs[i].jobId === pending.jobId)) {
		state.jobs[i] = Object.assign({}, state.jobs[i], pending);
		pending = state.jobs[i];
		replaced = true;
		break;
	  }
	}
	if (!replaced) state.jobs.push(pending);
	pending._savedAt = parseInt(pending._savedAt || pending.sentAt, 10) || Date.now();
	state.jobs.sort(function (a, b) { return (a.sequence || Number.MAX_SAFE_INTEGER) - (b.sequence || Number.MAX_SAFE_INTEGER) || a.sentAt - b.sentAt; });
	state.pending = state.jobs.length ? state.jobs[0] : null;
    var adapter = getPersistence();
    if (adapter) adapter.savePending(state.jobs);
    emit('pending:set', { pending: Object.assign({}, state.pending) });
  }

  function refreshPending() {
    if (!state.pending) return null;
    emit('pending:set', { pending: Object.assign({}, state.pending) });
    return state.pending;
  }

  function loadPending() {
    var adapter = getPersistence();
    if (!adapter) return null;
    var parsed = adapter.loadPending();
    if (!parsed) return null;
	state.jobs = Array.isArray(parsed) ? parsed : [parsed];
	state.pending = state.jobs.length ? state.jobs[0] : null;
	if (!persistenceSubscribed && typeof adapter.onPendingChange === 'function') {
	  persistenceSubscribed = true;
	  adapter.onPendingChange(function (jobs) {
		state.jobs = Array.isArray(jobs) ? jobs : [];
		state.pending = state.jobs.length ? state.jobs[0] : null;
		emit('pending:set', { pending: state.pending ? Object.assign({}, state.pending) : null });
	  });
	}
    emit('pending:set', { pending: Object.assign({}, state.pending) });
    return state.pending;
  }

  function clearPending(identity) {
	if (identity) {
	  state.jobs = state.jobs.filter(function (job) { return job.jobId !== identity && job.requestId !== identity; });
	} else if (state.pending) {
	  var current = state.pending;
	  state.jobs = state.jobs.filter(function (job) { return job !== current; });
	}
	state.pending = state.jobs.length ? state.jobs[0] : null;
    var adapter = getPersistence();
	if (adapter && typeof adapter.removePending === 'function') adapter.removePending(identity || (current && (current.jobId || current.requestId)) || '', state.jobs);
	else if (adapter) adapter.savePending(state.jobs);
    emit('pending:cleared', { identity: identity || '' });
	if (state.pending) emit('pending:set', { pending: Object.assign({}, state.pending) });
  }

	function getPendingJobs() {
	  return state.jobs.map(function (job) { return Object.assign({}, job); });
	}

  function shouldPersistLocalMessage(message) {
    if (!message) return false;
    return message.local === true || message.source === 'admin_ui' || message.source === 'admin_error';
  }

  function loadLocalMessages() {
    var adapter = getPersistence();
    var raw = adapter ? adapter.loadLocalMessages() : [];
    var out = [];
    for (var i = 0; i < raw.length; i++) {
      var msg = normalizeMessage(raw[i]);
      if (!msg) continue;
      msg.local = true;
      out.push(msg);
    }
    return normalizeList(out);
  }

  function writeLocalMessages(messages) {
    messages = normalizeList(messages || []);
    var adapter = getPersistence();
    if (adapter) adapter.writeLocalMessages(messages);
  }

  function persistLocalMessage(message) {
    if (!shouldPersistLocalMessage(message)) return;
    var msg = normalizeMessage(Object.assign({}, message, { local: true }));
    if (!msg) return;
    var existing = loadLocalMessages();
    for (var i = 0; i < existing.length; i++) {
      if (existing[i].id === msg.id) {
        return;
      }
    }
    existing.push(msg);
    writeLocalMessages(existing);
  }

  function pruneLocalMessagesAgainst(serverMessages) {
    var localMessages = loadLocalMessages();
    if (!localMessages.length) return [];

    var remaining = [];
    for (var i = 0; i < localMessages.length; i++) {
      if (hasMessageInList(serverMessages, localMessages[i])) continue;
      remaining.push(localMessages[i]);
    }
    writeLocalMessages(remaining);
    return remaining;
  }

  function setOpen(isOpen) {
    state.isOpen = !!isOpen;
    emit(state.isOpen ? 'chat:open' : 'chat:close', {});
  }

  function getPersistence() {
    if (!persistence && window.Niwa && window.Niwa.AdminChatPersistence) {
      persistence = window.Niwa.AdminChatPersistence;
    }
    return persistence;
  }

  return {
    on: on,
    getState: getState,
    replace: replace,
    prepend: prepend,
    append: append,
    appendMany: appendMany,
    setPending: setPending,
    refreshPending: refreshPending,
    loadPending: loadPending,
    clearPending: clearPending,
	getPendingJobs: getPendingJobs,
    setOpen: setOpen
  };
})();
