import { ContactInfo } from 'session_util_wrapper';
import { ConversationModel } from '../../../models/conversation';
import { getContactInfoFromDBValues } from '../../../types/sqlSharedTypes';
import { ContactsWrapperActions } from '../../../webworker/workers/browser/libsession_worker_interface';
import { getConversationController } from '../../conversations';
import { PubKey } from '../../types';

/**
 * This file is centralizing the management of data from the Contacts Wrapper of libsession.
 * It allows to make changes to the wrapper and keeps track of the decoded values of those in the in-memory cache named `mappedContactWrapperValues`.
 *
 * The wrapper content is just a blob which has no structure.
 * Rather than having to fetch the required data from it everytime we need it (during each rerendering), we keep a decoded cache here.
 * Essentially, on app start we load all the content from the wrapper with `SessionUtilContact.refreshMappedValue` during the ConversationController initial load.
 * Then, everytime we do a change to the contacts wrapper, we do it through `insertContactFromDBIntoWrapperAndRefresh`.
 * This applies the change from the in-memory conversationModel to the ContactsWrapper, refetch the data from it and update the decoded cache `mappedContactWrapperValues` with the up to date data.
 * It then triggers a UI refresh of that specific conversation with `triggerUIRefresh` to make sure whatever is displayed on screen is still up to date with the wrapper content.
 *
 * Also, to make sure that our wrapper is up to date, we schedule jobs to be run and fetch all contacts and update all the wrappers entries.
 * This is done in the
 *    - `ConfigurationSyncJob` (sending data to the network) and the
 *    - `ConfigurationSyncDumpJob` (just dumping locally the data)
 * with `insertAllContactsIntoContactsWrapper()`
 *
 */
const mappedContactWrapperValues = new Map<string, ContactInfo>();

/**
 * Update the ContactWrapper with all the data is cares about from the database.
 */
async function insertAllContactsIntoContactsWrapper() {
  const idsToInsert = getConversationController()
    .getConversations()
    .filter(isContactToStoreInContactsWrapper)
    .map(m => m.id);

  window.log.debug(`ContactsWrapper keep tracks of ${idsToInsert.length} contacts`);

  for (let index = 0; index < idsToInsert.length; index++) {
    const id = idsToInsert[index];

    await insertContactFromDBIntoWrapperAndRefresh(id);
  }
}

/**
 * Returns true if that conversation is not us, is private, is not blinded and has either the
 * `isApproved` or `didApproveMe` field set.
 * So that would be all the private conversations we either sent or receive a message from, not blinded
 */
function isContactToStoreInContactsWrapper(convo: ConversationModel): boolean {
  return (
    !convo.isMe() &&
    convo.isPrivate() &&
    convo.isActive() &&
    !PubKey.hasBlindedPrefix(convo.id) &&
    (convo.isApproved() || convo.didApproveMe())
  );
}

/**
 * Fetches the specified convo and updates the required field in the wrapper.
 * If that contact does not exist in the wrapper, it is created before being updated.
 */
// tslint:disable-next-line: cyclomatic-complexity
async function insertContactFromDBIntoWrapperAndRefresh(id: string): Promise<void> {
  const foundConvo = getConversationController().get(id);
  if (!foundConvo) {
    return;
  }

  if (!isContactToStoreInContactsWrapper(foundConvo)) {
    // window.log.info(`insertContactFromDBIntoWrapperAndRefresh: convo ${id} should not be saved. Skipping`);
    return;
  }

  const dbName = foundConvo.get('displayNameInProfile') || undefined;
  const dbNickname = foundConvo.get('nickname') || undefined;
  const dbProfileUrl = foundConvo.get('avatarPointer') || undefined;
  const dbProfileKey = foundConvo.get('profileKey') || undefined;
  const dbApproved = !!foundConvo.get('isApproved') || false;
  const dbApprovedMe = !!foundConvo.get('didApproveMe') || false;
  const dbBlocked = !!foundConvo.isBlocked() || false;
  const hidden = foundConvo.get('hidden') || false;
  const isPinned = foundConvo.get('isPinned');

  const wrapperContact = getContactInfoFromDBValues({
    id,
    dbApproved,
    dbApprovedMe,
    dbBlocked,
    dbName,
    dbNickname,
    dbProfileKey,
    dbProfileUrl,
    isPinned,
    hidden,
  });

  try {
    await ContactsWrapperActions.set(wrapperContact);
  } catch (e) {
    window.log.warn(`ContactsWrapperActions.set of ${id} failed with ${e.message}`);
    // we still let this go through
  }

  await refreshMappedValue(id);
}

/**
 * @param duringAppStart set this to true if we should just fetch the cached value but not trigger a UI refresh of the corresponding conversation
 */
async function refreshMappedValue(id: string, duringAppStart = false) {
  const fromWrapper = await ContactsWrapperActions.get(id);
  if (fromWrapper) {
    setMappedValue(fromWrapper);
    if (!duringAppStart) {
      getConversationController()
        .get(id)
        ?.triggerUIRefresh();
    }
  }
}

function setMappedValue(info: ContactInfo) {
  mappedContactWrapperValues.set(info.id, info);
}

function getContactCached(id: string) {
  return mappedContactWrapperValues.get(id);
}

export const SessionUtilContact = {
  isContactToStoreInContactsWrapper,
  insertAllContactsIntoContactsWrapper,
  insertContactFromDBIntoWrapperAndRefresh,
  getContactCached,
  refreshMappedValue,
};