import { Injectable, NgZone } from '@angular/core';
import { NavigationExtras } from '@angular/router';
import { Clipboard } from '@awesome-cordova-plugins/clipboard/ngx';
import { TranslateService } from '@ngx-translate/core';
import { Logger } from 'src/app/logger';
import { App } from "src/app/model/app.enum";
import { Contact as ContactNotifierContact, ContactNotifierService } from 'src/app/services/contactnotifier.service';
import { Events } from 'src/app/services/events.service';
import { GlobalDIDSessionsService, IdentityEntry } from 'src/app/services/global.didsessions.service';
import { GlobalIntentService } from 'src/app/services/global.intent.service';
import { GlobalNavService } from 'src/app/services/global.nav.service';
import { GlobalService, GlobalServiceManager } from 'src/app/services/global.service.manager';
import { GlobalStorageService } from 'src/app/services/global.storage.service';
import { defaultContacts } from '../config/config';
import { Avatar } from '../models/avatar';
import { Contact } from '../models/contact.model';
import { DidService } from './did.service';
import { NativeService } from './native.service';



declare let didManager: DIDPlugin.DIDManager;

@Injectable({
  providedIn: 'root'
})
export class FriendsService extends GlobalService {
  // Pending contact
  public pendingContact: Contact = {
    id: null,
    didDocument: null,
    credentials: null,
    avatarLocal: null,
    customName: null,
    customNote: null,
    isPicked: false,
    isFav: false,
    carrierAddress: null,
    notificationsCarrierAddress: null
  };

  // Stored contacts
  public contacts: Contact[] = [];

  // For intents filtering contacts
  public filteredContacts: Contact[] = [];

  // For friends page avatar slider
  public activeSlide: Contact;

  // For sorting contacts by first letter
  public letters: string[] = [];

  // Set first contact for first visit
  public firstVisit = false;

  // Check contacts on app load for updates
  public contactsChecked = false;

  public contactsFetched = false;

  // Temporary storage for an invitation id from a received "viewfriendinviation" intent
  public contactNotifierInviationId: string = null;

  // For intents
  public managerService: any;
  public shareIntentData: {
    title: string,
    url?: string
  } = null;

  getContact(id: string) {
    return { ...this.contacts.find(contact => contact.id === id) };
  }

  constructor(
    private globalNav: GlobalNavService,
    public zone: NgZone,
    private clipboard: Clipboard,
    public translate: TranslateService,
    private native: NativeService,
    private storage: GlobalStorageService,
    private events: Events,
    private didService: DidService,
    private contactNotifier: ContactNotifierService,
    private globalIntentService: GlobalIntentService,
  ) {
    super();
    GlobalServiceManager.getInstance().registerService(this);
    this.managerService = this;
  }

  onUserSignIn(signedInIdentity: IdentityEntry): Promise<void> {
    return;
  }

  onUserSignOut(): Promise<void> {
    this.resetService();
    return;
  }

  private resetService() {
    this.pendingContact = null;
    this.contacts = [];
    this.filteredContacts = [];
    this.contactsChecked = false;
    this.contactsFetched = false;
  }

  async init() {
    await this.getStoredContacts();
    void this.checkFirstVisitOperations(); // Non blocking add of default contacts in background
    this.getContactNotifierContacts();
  }

  /******************************************************
  * Check if it's the first time user enters contacts.
  * If so, add a few default fake contacts.
  *******************************************************/
  async checkFirstVisitOperations() {
    let visit = await this.storage.getSetting<boolean>(GlobalDIDSessionsService.signedInDIDString, "contacts", 'visited', false);
    if (!visit) {
      await this.addDefaultContacts();
      await this.storage.setSetting(GlobalDIDSessionsService.signedInDIDString, 'contacts', 'visited', true)
    }
  }

  /**
   * Adds a few default contacts to the contacts list, so that user feels he is not alone.
   */
  private async addDefaultContacts(): Promise<void> {
    // Show a loader - this may take a while
    Logger.log("contacts", "Adding default contacts", defaultContacts);
    for (let contactDID of defaultContacts) {
      // Don't await to resolve each contact  one by one. This may work in parrallel and not
      // block the boot sequence.
      await this.resolveDIDDocument(contactDID, false, null, false);
    }
    Logger.log("contacts", "Default contacts were added");
    return;
  }

  /******************************
  **** Fetch Stored Contacts ****
  *******************************/
  async getStoredContacts(): Promise<Contact[]> {
    Logger.log("contacts", "Getting stored contacts for DID ", GlobalDIDSessionsService.signedInDIDString);
    let contacts = await this.storage.getSetting(GlobalDIDSessionsService.signedInDIDString, "contacts", "contacts", []);
    Logger.log("Contacts", 'Stored contacts fetched', contacts);
    this.contactsFetched = true;

    if (contacts) {
      this.contacts = contacts;
      await this.sortContacts();
      void this.checkContacts();
      return contacts || [];
    } else {
      Logger.log('contacts', "No stored contacts");
      return [];
    }
  }

  private async checkContacts(): Promise<void> {
    if (!this.contactsChecked) {
      this.contactsChecked = true;
      for (let contact of this.contacts) {
        Logger.log("Contacts", 'Checking stored contact for updates', contact);
        contact.id !== 'did:elastos' ? await this.resolveDIDDocument(contact.id, true) : null;
      }
    }
  }

  /************************************************
  *** Add Unadded Contacts from Contact Notifier ***
  *************************************************/
  getContactNotifierContacts() {
    void this.contactNotifier.getAllContacts().then(async (notifierContacts) => {
      //Logger.log("Contacts", 'Found all Notifier Contacts', notifierContacts);
      notifierContacts.forEach((notifierContact) => {
        const alreadyAddedContact = this.contacts.find((contact) => contact.id === notifierContact.getDID());
        if (!alreadyAddedContact) {
          const contactAvatar = notifierContact.getAvatar();
          const newContact: Contact = {
            id: notifierContact.getDID(),
            didDocument: {
              clazz: null,
              id: {
                storeId: null,
                didString: notifierContact.getDID(),
              },
              created: null,
              updated: null,
              verifiableCredential: [],
              publicKey: null,
              authentication: null,
              authorization: null,
              expires: null,
              storeId: null,
            },
            credentials: {
              name: notifierContact.getName(),
              gender: null,
              nickname: null,
              nation: null,
              birthDate: null,
              birthPlace: null,
              occupation: null,
              education: null,
              telephone: null,
              email: null,
              interests: null,
              description: null,
              url: null,
              twitter: null,
              facebook: null,
              instagram: null,
              snapchat: null,
              telegram: null,
              wechat: null,
              weibo: null,
              twitch: null,
              elaAddress: null,
              avatar: contactAvatar && Object.getOwnPropertyNames(contactAvatar).length !== 0 ? Avatar.fromContactNotifierContactAvatar(contactAvatar) : null
            },
            avatarLocal: null,
            customName: null,
            customNote: null,
            isPicked: false,
            isFav: false,
            carrierAddress: notifierContact.getCarrierUserID(),
            notificationsCarrierAddress: null
          }

          this.safeAddContact(newContact);
        } else {
          Logger.log('contacts', 'Contact Notifier Contact', alreadyAddedContact + ' is already added');
        }
      });

      await this.saveContactsState();
    });
  }

  /************************************************
  *********** Add Friend By Scan Button ***********
  *************************************************/
  async scanDID() {
    let res = await this.globalIntentService.sendIntent("https://scanner.elastos.net/scanqrcode");
    Logger.log('contacts', "Got scan result", res);

    // Scanned content could contain different things:
    // - A did: did:elastos:xxxx
    // - A add friend url: https://contact.elastos.net/addfriend?did=xxx[&carrier=xxx]
    // - Something that we don't know
    let scannedContentHandled = false
    if (res && res.result && res.result.scannedContent) {
      let scannedContent = res.result.scannedContent;

      if (scannedContent.indexOf("did:") == 0) {
        // We've scanned a DID string. Add friend, without carrier address support
        Logger.log('contacts', "Scanned content is a DID string");
        void this.addContactByIntent(scannedContent, null);
        scannedContentHandled = true;
      }
      else if (scannedContent.indexOf("http") == 0) {
        Logger.log('contacts', "Scanned content is a URL");
        // Probably a url - try to parse it and see if we can handle it
        let scannedUrl = new URL(scannedContent);
        Logger.log('contacts', scannedUrl);

        if (scannedUrl) {
          if (scannedUrl.pathname == "/addfriend") {
            let did = scannedUrl.searchParams.get("did");
            let carrierAddress = scannedUrl.searchParams.get("carrier");

            void this.addContactByIntent(did, carrierAddress);
            scannedContentHandled = true;
          }
        }
      }
    }

    if (!scannedContentHandled) {
      void this.native.genericToast(this.translate.instant('contacts.failed-read-scan'));
    }
  }

  /*******************************************
  *********** Add Contact By Intent ***********
  ********************************************/
  async addContactByIntent(did: string, carrierAddress?: string) {
    Logger.log('contacts', 'Received contact by intent', did, carrierAddress);

    if (this.didService.getUserDID() === did) {
      void this.native.genericToast('contacts.please-dont-add-self');
      void this.globalNav.navigateRoot('contacts', '/contacts/friends');
    } else {
      const targetContact: Contact = this.contacts.find(contact => contact.id === did);
      if (targetContact) {
        const promptName = this.getPromptName(targetContact);

        if (carrierAddress) {
          this.contacts[this.contacts.indexOf(targetContact)].notificationsCarrierAddress = carrierAddress;
          await this.storage.setSetting(GlobalDIDSessionsService.signedInDIDString, "contacts", "contacts", this.contacts);
          void this.globalNav.navigateRoot('contacts', '/contacts/friends/' + targetContact.id);
          void this.native.genericToast(promptName + this.translate.instant('contacts.did-carrier-added'));
          Logger.log('contacts', 'Contact is already added but carrier address is updated', this.contacts[this.contacts.indexOf(targetContact)]);
        } else {
          void this.native.genericToast(promptName + this.translate.instant('contacts.is-already-added'));
          void this.globalNav.navigateRoot('contacts', '/contacts/friends/' + targetContact.id);
          Logger.log('contacts', 'Contact is already added');
        }
      } else {
        void this.native.showLoading(this.translate.instant('common.please-wait'));
        await this.resolveDIDDocument(did, false, carrierAddress);
        void this.native.hideLoading();
      }
    }
  }

  /******************************************************************
   * From a DID string, try to resolve the published DID document
   * from the DID sidechain. That DID document may or may not include
   * BasicProfileCredential types credentials such as "name", "email",
   * "telephone", and also ApplicationProfileCredential type credentials
   * that have earlier been registered through "registerapplicationprofile"
   * intents, by the DID app, on request from third party apps. This is
   * where we can retrieve public app profile information for a "user" (DID).
  ****************************************************************************/
  resolveDIDDocument(
    didString: DIDPlugin.DIDString,
    updatingFriends: boolean,
    carrierAddress?: string,
    requiresConfirmation?: boolean,
  ): Promise<void> {
    Logger.log("Contacts",
      'Resolving DID document for DID string ', didString,
      'Updating friends?' + updatingFriends,
      'Requires confirmation?' + requiresConfirmation
    );
    return new Promise((resolve, reject) => {
      // eslint-disable-next-line @typescript-eslint/no-misused-promises
      didManager.resolveDidDocument(didString, true, async (didDocument: DIDPlugin.DIDDocument) => {
        Logger.log("Contacts", "DIDDocument resolved for DID " + didString, didDocument);
        if (didDocument && !updatingFriends) {
          await this.buildPublishedContact(didDocument, carrierAddress, requiresConfirmation);
        } else if (didDocument && updatingFriends) {
          await this.updateContact(didDocument);
        } else if (!didDocument && updatingFriends) {
          return;
        } else {
          void this.native.genericToast(this.translate.instant('contacts.contact-is-unpublished'));
          await this.buildUnpublishedContact(didString, carrierAddress, requiresConfirmation);
        }

        resolve();
      }, (err: any) => {
        Logger.error('contacts', "DIDDocument resolving error", err);
        resolve();
      });
    });
  }

  /************************************************
  ***** Update Contact's Credentials on App Load  *
  *************************************************/
  async updateContact(newDoc): Promise<void> {
    for (let contact of this.contacts) {
      if (contact.id === newDoc.id.didString) {
        Logger.log("Contacts", 'Updating contact', contact);

        contact.didDocument = newDoc;
        for (let key of newDoc.verifiableCredential) {
          if ('name' in key.credentialSubject) {
            contact.credentials.name = key.credentialSubject.name;
          }
          if ('gender' in key.credentialSubject) {
            contact.credentials.gender = key.credentialSubject.gender;
          }
          if ('avatar' in key.credentialSubject) {
            contact.credentials.avatar = await Avatar.fromAvatarCredential(key.credentialSubject.avatar);
          }
          if ('nickname' in key.credentialSubject) {
            contact.credentials.nickname = key.credentialSubject.nickname;
          }
          if ('nationality' in key.credentialSubject) {
            contact.credentials.nation = key.credentialSubject.nation;
          }
          if ('birthDate' in key.credentialSubject) {
            contact.credentials.birthDate = key.credentialSubject.birthDate;
          }
          if ('birthPlace' in key.credentialSubject) {
            contact.credentials.birthPlace = key.credentialSubject.birthPlace;
          }
          if ('occupation' in key.credentialSubject) {
            contact.credentials.occupation = key.credentialSubject.occupation;
          }
          if ('education' in key.credentialSubject) {
            contact.credentials.education = key.credentialSubject.education;
          }
          if ('telephone' in key.credentialSubject) {
            contact.credentials.telephone = key.credentialSubject.telephone;
          }
          if ('email' in key.credentialSubject) {
            contact.credentials.email = key.credentialSubject.email;
          }
          if ('interests' in key.credentialSubject) {
            contact.credentials.interests = key.credentialSubject.interests;
          }
          if ('description' in key.credentialSubject) {
            contact.credentials.description = key.credentialSubject.description;
          }
          if ('url' in key.credentialSubject) {
            contact.credentials.url = key.credentialSubject.url;
          }
          if ('twitter' in key.credentialSubject) {
            contact.credentials.twitter = key.credentialSubject.twitter;
          }
          if ('facebook' in key.credentialSubject) {
            contact.credentials.facebook = key.credentialSubject.facebook;
          }
          if ('instagram' in key.credentialSubject) {
            contact.credentials.instagram = key.credentialSubject.instagram;
          }
          if ('snapchat' in key.credentialSubject) {
            contact.credentials.snapchat = key.credentialSubject.snapchat;
          }
          if ('telegram' in key.credentialSubject) {
            contact.credentials.telegram = key.credentialSubject.telegram;
          }
          if ('wechat' in key.credentialSubject) {
            contact.credentials.wechat = key.credentialSubject.wechat;
          }
          if ('weibo' in key.credentialSubject) {
            contact.credentials.weibo = key.credentialSubject.weibo;
          }
          if ('twitch' in key.credentialSubject) {
            contact.credentials.twitch = key.credentialSubject.twitch;
          }
          if ('elaAddress' in key.credentialSubject) {
            contact.credentials.elaAddress = key.credentialSubject.elaAddress;
          }
        }

        await this.saveContactsState();
        this.updateNotifierContact(contact);
      }
    }
  }

  /******************************************************
  ** Reset Pending Contact for Unresolved/Resolved DID **
  *******************************************************/
  resetPendingContact(didString: string, carrierString?: string) {
    this.pendingContact = {
      id: didString,
      didDocument: {
        clazz: null,
        id: {
          storeId: null,
          didString: didString
        },
        created: null,
        updated: null,
        verifiableCredential: [],
        publicKey: null,
        authentication: null,
        authorization: null,
        expires: null,
        storeId: null,
      },
      credentials: {
        name: null,
        gender: null,
        nickname: null,
        nation: null,
        birthDate: null,
        birthPlace: null,
        occupation: null,
        education: null,
        telephone: null,
        email: null,
        interests: null,
        description: null,
        url: null,
        twitter: null,
        facebook: null,
        instagram: null,
        snapchat: null,
        telegram: null,
        wechat: null,
        weibo: null,
        twitch: null,
        elaAddress: null,
        avatar: null
      },
      avatarLocal: null,
      customName: null,
      customNote: null,
      isPicked: false,
      isFav: false,
      carrierAddress: null,
      notificationsCarrierAddress: carrierString ? carrierString : null
    };
    Logger.log('contacts', 'Pending contact is reset', this.pendingContact);
  }

  /********************************************************
  **** Start Filling Pending Contact for Unresolved DID ***
  *********************************************************/
  async buildUnpublishedContact(didString: string, carrierString?: string, requiresConfirmation?: boolean): Promise<void> {
    Logger.log('contacts', 'Building contact using unresolved DID for confirm-prompt', didString);
    this.resetPendingContact(didString, carrierString);

    if (requiresConfirmation === false) {
      this.safeAddContact(this.pendingContact);
      await this.saveContactsState();
    } else {
      this.showConfirmPrompt(false);
    }
  }

  /*******************************************************
  **** Start Filling Current Contact for Resolved DID *****
  *********************************************************/
  async buildPublishedContact(resolvedDidDocument, carrierString?: string, requiresConfirmation?: boolean): Promise<void> {
    Logger.log('contacts', 'Building contact using resolved DID document for confirm-prompt', resolvedDidDocument);
    const resolvedDidString = resolvedDidDocument.id.didString;
    this.resetPendingContact(resolvedDidString, carrierString);

    this.pendingContact.didDocument = resolvedDidDocument;
    this.pendingContact.id = resolvedDidString;

    for (let key of resolvedDidDocument.verifiableCredential) {
      if ('name' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has name');
        this.pendingContact.credentials.name = key.credentialSubject.name;
      }
      if ('avatar' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has avatar');
        this.pendingContact.credentials.avatar = await Avatar.fromAvatarCredential(key.credentialSubject.avatar);
      }
      if ('nickname' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has nickname');
        this.pendingContact.credentials.nickname = key.credentialSubject.nickname;
      }
      if ('gender' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has gender');
        this.pendingContact.credentials.gender = key.credentialSubject.gender;
      }
      if ('nationality' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has nation');
        this.pendingContact.credentials.nation = key.credentialSubject.nation;
      }
      if ('birthDate' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has birth date');
        this.pendingContact.credentials.birthDate = key.credentialSubject.birthDate;
      }
      if ('birthPlace' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has birth place');
        this.pendingContact.credentials.birthPlace = key.credentialSubject.birthPlace;
      }
      if ('occupation' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has occupation');
        this.pendingContact.credentials.occupation = key.credentialSubject.occupation;
      }
      if ('education' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has education');
        this.pendingContact.credentials.education = key.credentialSubject.education;
      }
      if ('telephone' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has telephone');
        this.pendingContact.credentials.telephone = key.credentialSubject.telephone;
      }
      if ('email' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has email');
        this.pendingContact.credentials.email = key.credentialSubject.email;
      }
      if ('interests' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has interests');
        this.pendingContact.credentials.interests = key.credentialSubject.interests;
      }
      if ('description' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has description');
        this.pendingContact.credentials.description = key.credentialSubject.description;
      }
      if ('url' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has website');
        this.pendingContact.credentials.url = key.credentialSubject.url;
      }
      if ('twitter' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has twitter');
        this.pendingContact.credentials.twitter = key.credentialSubject.twitter;
      }
      if ('facebook' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has facebook');
        this.pendingContact.credentials.facebook = key.credentialSubject.facebook;
      }
      if ('instagram' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has instagram');
        this.pendingContact.credentials.instagram = key.credentialSubject.instagram;
      }
      if ('snapchat' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has snapchat');
        this.pendingContact.credentials.snapchat = key.credentialSubject.snapchat;
      }
      if ('telegram' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has telegram');
        this.pendingContact.credentials.telegram = key.credentialSubject.telegram;
      }
      if ('wechat' in key.credentialSubject) {
        Logger.log('contacts', 'Resolved DID has wechat');
        this.pendingContact.credentials.wechat = key.credentialSubject.wechat;
      }
      if ('weibo' in key.credentialSubject) {
        Logger.log('contacts', 'Contact has weibo');
        this.pendingContact.credentials.weibo = key.credentialSubject.weibo;
      }
      if ('twitch' in key.credentialSubject) {
        Logger.log('contacts', 'Contact has twitch');
        this.pendingContact.credentials.twitch = key.credentialSubject.twitch;
      }
      if ('elaAddress' in key.credentialSubject) {
        Logger.log('contacts', 'Contact has ela wallet');
        this.pendingContact.credentials.elaAddress = key.credentialSubject.elaAddress;
      }
    }

    if (requiresConfirmation === false) {
      this.safeAddContact(this.pendingContact);
      await this.saveContactsState();
    } else {
      this.showConfirmPrompt(true);
    }
  }

  /********************************************************
  **************** Prompt Confirm Contact ******************
  *********************************************************/
  showConfirmPrompt(isPublished: boolean) {
    Logger.log('contacts', "Prompting contact confirm", this.pendingContact);
    const props: NavigationExtras = {
      queryParams: {
        id: this.pendingContact.id,
        name: this.pendingContact.credentials.name,
        image: this.pendingContact.credentials.avatar ? JSON.stringify(this.pendingContact.credentials.avatar) : null, // Temporary BPI fix to avoid receiving [Object object] in the confirm screen, but better avoid using query params for potentially large data like avatars. Need to fix here @chad.
        isPublished: isPublished,
      }
    }
    void this.globalNav.navigateRoot('contacts', '/contacts/confirm', props);
  }

  /********************************************************
  ******** Finalize Add Contact If Confirmed By User *******
  *********************************************************/
  addContact(): Promise<boolean> {
    // eslint-disable-next-line @typescript-eslint/no-misused-promises, no-async-promise-executor
    return new Promise(async (resolve, reject) => {
      const promptName = this.getPromptName(this.pendingContact);
      const targetContact: Contact = this.contacts.find(contact => contact.id === this.pendingContact.id);

      if (targetContact) {
        if (this.pendingContact.carrierAddress) {
          this.contacts[this.contacts.indexOf(targetContact)].carrierAddress = this.pendingContact.carrierAddress;

          // Modify contact in backup
          this.events.publish("backup:contact", this.contacts[this.contacts.indexOf(targetContact)]);

          void this.native.genericToast(promptName + this.translate.instant('contacts.did-carrier-added'));
          Logger.log('contacts', 'Contact is already added but carrier address is updated');
        } else {
          void this.native.genericToast(promptName + this.translate.instant('contacts.is-already-added'));
          Logger.log('contacts', 'Contact is already added');
        }
        resolve(true);
      } else {
        // If a carrier address was provided with a addfriend intent, we use this friend's carrier address
        // To try to reach him also through contact notifier plugin's global carrier address.
        // After he accepts this invitation, it becomes possible to send him remote notifications.
        if (this.pendingContact.notificationsCarrierAddress) {
          Logger.log('contacts', "Sending friend invitation through contact notifier");
          void this.contactNotifier.sendInvitation(
            this.pendingContact.id,
            this.pendingContact.notificationsCarrierAddress
          );
        } else {
          Logger.log('contacts', "Added friend has no associated contact notifier carrier address");
        }

        if (this.contactNotifierInviationId) {
          Logger.log('contacts', 'Accepting contact notifier invitation', this.contactNotifierInviationId);
          void this.contactNotifier.acceptInvitation(this.contactNotifierInviationId);
          this.contactNotifierInviationId = null;
        } else {
          Logger.log('contacts', 'Confirmed contact did not come from a "viewfriendinvitation" intent');
        }

        this.safeAddContact(this.pendingContact);
        this.updateNotifierContact(this.pendingContact);

        // Add contact in backup
        this.events.publish("backup:contact", this.pendingContact);

        void this.native.genericToast(promptName + this.translate.instant('contacts.was-added'));
        resolve(false);
      }

      await this.saveContactsState();
    });
  }

  /**
   * Adds a contact to the global contacts array, but first makes sure that the contact (by DID)
   * doesn't already exit yet to be robust against any logic mistake.
   */
  public safeAddContact(contact: Contact) {
    if (this.contacts.find(c => c.id === contact.id)) {
      Logger.warn("contacts", "Trying to add contact that already exists in the list! Logic error", contact, this.contacts);
      return;
    }

    this.contacts.push(contact);
  }

  /********************************************************
  ******** Add/Update Contacts in Notifier Contacts ********
  *********************************************************/
  updateNotifierContact(contact: Contact) {
    void this.contactNotifier.resolveContact(contact.id).then(
      (notifierContact: ContactNotifierContact) => {
        if (notifierContact) {
          let targetAvatar: Avatar = null;
          let targetName: string = null;

          if (contact.avatarLocal) {
            targetAvatar = contact.avatarLocal;
          } else if (contact.credentials.avatar) {
            targetAvatar = contact.credentials.avatar;
          }
          if (contact.customName) {
            targetName = contact.customName;
          } else if (contact.credentials.name) {
            targetName = contact.credentials.name;
          }

          if (targetAvatar) {
            Logger.log('contacts', 'Updating notifier contact avatar' + contact.id);
            void notifierContact.setAvatar({
              contentType: targetAvatar.contentType,
              base64ImageData: targetAvatar.data
            });
          }
          if (targetName) {
            Logger.log('contacts', 'Updating notifier contact name' + contact.id);
            void notifierContact.setName(targetName);
          }
        }
      });
  }

  /********************************************************
  *************** Finalize Delete Contact *****************
  *********************************************************/
  async deleteContact(contact: Contact) {
    const promptName = this.getPromptName(contact);

    Logger.log('contacts', "Deleting contact from the contact notifier database");
    await this.contactNotifier.removeContact(contact.id);

    Logger.log('contacts', 'Deleting contact', contact);
    this.contacts = this.contacts.filter(_contact => _contact.id !== contact.id);

    Logger.log('contacts', 'Updated contacts after deleting:' + contact.credentials.name, this.contacts);
    await this.saveContactsState();

    // Update home page contact slides
    this.events.publish('friends:updateSlider');

    // Delete contact in backup
    this.events.publish("backup:deleteContact", contact);

    void this.native.genericToast(promptName + this.translate.instant('contacts.was-deleted'));
    void this.globalNav.navigateRoot('contacts', '/contacts/friends');
  }

  /**
  * If contact was deleted from slides, change active slide to next index of array
  * If contact of next index doesn't exist, change active slide to previous index
  **/
  updateContactsSlide(contact: Contact) {
    const replacedSlide = this.contacts[this.contacts.indexOf(contact) + 1];
    if (replacedSlide) {
      this.activeSlide = replacedSlide
    } else {
      this.activeSlide = this.contacts[this.contacts.indexOf(contact) - 1];
    }
    Logger.log('contacts', 'Active slide after deletion', this.activeSlide);
  }

  /********************************************************
  ************** Finalize Customize Contact ***************
  *********************************************************/
  async customizeContact(id: string, customName: string, customNote: string, customAvatar: Avatar) {
    for (let contact of this.contacts) {
      if (contact.id === id) {
        Logger.log("Contacts", 'Updating contact\'s custom values' + customName + customNote + customAvatar);

        contact.customName = customName;
        contact.customNote = customNote;
        contact.avatarLocal = customAvatar;

        await this.saveContactsState();
        this.events.publish("backup:contact", contact);
      }
    }

    void this.globalNav.navigateRoot(App.CONTACTS, '/contacts/friends/' + id);
  }

  /********************************************************
  ************* Handle 'viewfriend' Intent ****************
  *********************************************************/
  viewContact(didString: string) {
    void this.getStoredContacts().then(async (contacts: Contact[]) => {
      const targetContact = contacts.find((contact) => contact.id === didString);
      if (targetContact) {
        void this.globalNav.navigateTo('contacts', '/contacts/friends/' + didString);
      } else {
        await this.resolveDIDDocument(didString, false);
      }
    });
  }

  /********************************************************
  ************* Handle 'pickfriend'Intent *****************
  *********************************************************/

  // 'pickfriend' intent without filter param
  getContacts(isSingleInvite: boolean, intent: string) {
    void this.getStoredContacts().then((contacts: Contact[]) => {
      Logger.log('contacts', 'Fetched stored contacts for pickfriend intent', contacts);
      const realContacts = contacts.filter((contact) => contact.id !== 'did:elastos');
      if (realContacts.length > 0) {
        let props: NavigationExtras = {
          queryParams: {
            singleInvite: isSingleInvite,
            intent: intent
          }
        }
        void this.globalNav.navigateTo('contacts', '/contacts/invite', props);
      } else {
        void this.globalNav.navigateRoot('contacts', '/contacts/friends');
        void this.native.alertNoContacts(
          intent,
          this.managerService.handledIntentId,
          this.translate.instant('contacts.no-contacts-alert')
        );
      }
    });
  }

  // 'pickfriend' intent with filter param
  getFilteredContacts(isSingleInvite: boolean, ret) {
    void this.getStoredContacts().then((contacts: Contact[]) => {
      Logger.log('contacts', 'Fetched stored contacts for pickfriend intent', contacts);
      const realContacts = contacts.filter((contact) => contact.id !== 'did:elastos');
      if (realContacts.length > 0) {
        this.filteredContacts = [];

        Logger.log('contacts', 'Intent requesting friends with credential', ret.params.filter.credentialType);
        realContacts.map((contact) => {
          if (contact.credentials[ret.params.filter.credentialType]) {
            this.filteredContacts.push(contact);
          }
        });

        if (this.filteredContacts.length > 0) {
          let props: NavigationExtras = {
            queryParams: {
              singleInvite: isSingleInvite,
              friendsFiltered: true,
              intent: 'pickfriend'
            }
          }
          void this.globalNav.navigateTo('contacts', '/contacts/invite', props);
        } else {
          void this.globalNav.navigateRoot('friends', '/contacts/friends');
          void this.native.alertNoContacts(
            'pickfriend',
            this.managerService.handledIntentId,
            this.translate.instant('contacts.no-contacts-with-cred-alert')
          );
        }
      } else {
        void this.globalNav.navigateRoot('contacts', '/contacts/friends');
        void this.native.alertNoContacts(
          'pickfriend',
          this.managerService.handledIntentId,
          this.translate.instant('contacts.no-contacts-alert')
        );
        return;
      }
    });
  }

  async sendRemoteNotificationToContact(contactId: string, title: string, url: string) {
    let contactNotifierContact = await this.contactNotifier.resolveContact(contactId);
    if (contactNotifierContact) {
      Logger.log('contacts', "Sending shared content to friend with DID " + contactId);
      await contactNotifierContact.sendRemoteNotification({
        title: "Shared content from a contact",
        message: title,
        url: url
      });
    }
    else {
      Logger.warn('contacts', "Not sending shared content to friend with DID " + contactId + " because he is not in the contact notifier");
    }
  }

  async shareToContacts(isFilter: boolean) {
    Logger.log('contacts', "Sharing to contacts");

    let sentNotificationsCount = 0;
    if (!isFilter) {
      await Promise.all(this.contacts.map(async (contact) => {
        if (contact.isPicked) {
          await this.sendRemoteNotificationToContact(contact.id, this.shareIntentData.title, this.shareIntentData.url);
          contact.isPicked = false;
          sentNotificationsCount++;
        }
      }));
    } else {
      await Promise.all(this.filteredContacts.map(async (contact) => {
        if (contact.isPicked) {
          await this.sendRemoteNotificationToContact(contact.id, this.shareIntentData.title, this.shareIntentData.url);
          contact.isPicked = false;
          sentNotificationsCount++;
        }
      }));
    }
    Logger.log('contacts', "Tried to send " + sentNotificationsCount + " notifications to friends");
    Logger.log('contacts', "Sending share intent response");
    void this.globalIntentService.sendIntentResponse({},
      this.managerService.handledIntentId
    );
  }

  inviteContacts(isFilter: boolean, intent: string) {
    Logger.log('contacts', 'Invited filtered friends?', isFilter);
    let contactsForIntent = [];

    if (!isFilter) {
      contactsForIntent = this.contacts.filter((contact) => contact.isPicked);
      this.contacts.forEach((contact) => contact.isPicked = false);
    } else {
      contactsForIntent = this.filteredContacts.filter((contact) => contact.isPicked);
      this.filteredContacts.forEach((contact) => contact.isPicked = false);
    }

    Logger.log('contacts', 'Invited Contacts', contactsForIntent);
    this.sendIntentRes(contactsForIntent, intent);
  }

  sendIntentRes(contacts: Contact[], intent: string) {
    if (contacts.length > 0) {
      void this.globalIntentService.sendIntentResponse(
        { friends: contacts },
        this.managerService.handledIntentId
      );
    } else {
      void this.native.genericToast(this.translate.instant('contacts.select-before-invite'));
    }
  }

  // Send empty intent response when user cancel the action.
  sendEmptyIntentRes() {
    void this.globalIntentService.sendIntentResponse(
      {},
      this.managerService.handledIntentId, false
    );
  }

  /********************************************************
  ************* Manage Favorite Contacts ******************
  *********************************************************/
  async toggleFav(contact: Contact) {
    contact.isFav = !contact.isFav;
    await this.storage.setSetting(GlobalDIDSessionsService.signedInDIDString, "contacts", "contacts", this.contacts);
  }

  /********************************************************
  ********************* Share Contact *********************
  *********************************************************/
  async shareContact(contact: Contact) {
    let link = 'https://contact.elastos.net/addfriend?did=' + contact.id;
    await this.clipboard.copy(link);
    void this.native.shareToast();
  }

  /********************************************************
  ************** Handle Contact Buttons *******************
  *********************************************************/
  showCustomization(contact: Contact, contactAddedWithNoName: boolean) {
    const props: NavigationExtras = {
      queryParams: {
        id: contact.id,
        name: contact.credentials.name,
        avatar: JSON.stringify(contact.avatarLocal),
        customName: contact.customName,
        customNote: contact.customNote,
        contactAddedWithNoName: contactAddedWithNoName,
      }
    }
    void this.globalNav.navigateRoot(App.CONTACTS, '/contacts/customize', props);
  }

  /********************************************************
  ************* Sort Contacts Alphabetically **************
  *********************************************************/
  sortContacts() {
    this.letters = [];
    this.contacts.map((contact) => {
      // Add letter: 'anonymous'
      if (
        !contact.credentials.name && contact.customName && contact.customName === 'Anonymous Contact' && !this.letters.includes('Anonymous') ||
        !contact.credentials.name && !contact.customName && !this.letters.includes('Anonymous')
      ) {
        this.letters.push('Anonymous');
      }
      // Add first letter: contact name credential
      if (
        contact.credentials.name && !contact.customName && !this.letters.includes(contact.credentials.name[0].toUpperCase())
      ) {
        this.letters.push(contact.credentials.name[0].toUpperCase());
      }
      // Add first letter: contact custom name
      if (
        !contact.credentials.name && contact.customName && contact.customName !== 'Anonymous Contact' && !this.letters.includes(contact.customName[0].toUpperCase()) ||
        contact.credentials.name && contact.customName && contact.customName !== 'Anonymous Contact' && !this.letters.includes(contact.customName[0].toUpperCase())
      ) {
        this.letters.push(contact.customName[0].toUpperCase());
      }
    });

    this.letters = this.letters.sort((a, b) => a > b ? 1 : -1);
    this.letters.push(this.letters.splice(this.letters.indexOf('Anonymous'), 1)[0]);
  }

  getPromptName(contact: Contact): string {
    if (contact.customName) {
      return contact.customName
    } else if (contact.credentials.name) {
      return contact.credentials.name;
    } else {
      return this.translate.instant('contacts.anonymous-contact');
    }
  }

  async saveContactsState() {
    await this.storage.setSetting(GlobalDIDSessionsService.signedInDIDString, "contacts", "contacts", this.contacts);
    this.sortContacts();
  }
}