// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only

import type { ProfileKeyCredentialRequestContext } from '@signalapp/signal-client/zkgroup';
import { SEALED_SENDER } from '../types/SealedSender';
import { Address } from '../types/Address';
import { QualifiedAddress } from '../types/QualifiedAddress';
import * as Bytes from '../Bytes';
import { trimForDisplay, verifyAccessKey, decryptProfile } from '../Crypto';
import {
  generateProfileKeyCredentialRequest,
  getClientZkProfileOperations,
  handleProfileKeyCredential,
} from './zkgroup';
import { getSendOptions } from './getSendOptions';
import { isMe } from './whatTypeOfConversation';
import * as log from '../logging/log';
import { getUserLanguages } from './userLanguages';
import { parseBadgesFromServer } from '../badges/parseBadgesFromServer';

export async function getProfile(
  providedUuid?: string,
  providedE164?: string
): Promise<void> {
  if (!window.textsecure.messaging) {
    throw new Error(
      'Conversation.getProfile: window.textsecure.messaging not available'
    );
  }

  const { updatesUrl } = window.SignalContext.config;
  if (typeof updatesUrl !== 'string') {
    throw new Error('getProfile expected updatesUrl to be a defined string');
  }

  const id = window.ConversationController.ensureContactIds({
    uuid: providedUuid,
    e164: providedE164,
  });
  const c = window.ConversationController.get(id);
  if (!c) {
    log.error('getProfile: failed to find conversation; doing nothing');
    return;
  }

  const clientZkProfileCipher = getClientZkProfileOperations(
    window.getServerPublicParams()
  );

  const userLanguages = getUserLanguages(
    navigator.languages,
    window.getLocale()
  );

  let profile;

  try {
    await Promise.all([
      c.deriveAccessKeyIfNeeded(),
      c.deriveProfileKeyVersionIfNeeded(),
    ]);

    const profileKey = c.get('profileKey');
    const uuid = c.getCheckedUuid('getProfile');
    const profileKeyVersionHex = c.get('profileKeyVersion');
    if (!profileKeyVersionHex) {
      throw new Error('No profile key version available');
    }
    const existingProfileKeyCredential = c.get('profileKeyCredential');

    let profileKeyCredentialRequestHex: undefined | string;
    let profileCredentialRequestContext:
      | undefined
      | ProfileKeyCredentialRequestContext;

    if (profileKey && profileKeyVersionHex && !existingProfileKeyCredential) {
      log.info('Generating request...');
      ({
        requestHex: profileKeyCredentialRequestHex,
        context: profileCredentialRequestContext,
      } = generateProfileKeyCredentialRequest(
        clientZkProfileCipher,
        uuid.toString(),
        profileKey
      ));
    }

    const { sendMetadata = {} } = await getSendOptions(c.attributes);
    const getInfo = sendMetadata[uuid.toString()] || {};

    if (getInfo.accessKey) {
      try {
        profile = await window.textsecure.messaging.getProfile(uuid, {
          accessKey: getInfo.accessKey,
          profileKeyVersion: profileKeyVersionHex,
          profileKeyCredentialRequest: profileKeyCredentialRequestHex,
          userLanguages,
        });
      } catch (error) {
        if (error.code === 401 || error.code === 403) {
          log.info(
            `Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
          );
          c.set({ sealedSender: SEALED_SENDER.DISABLED });
          profile = await window.textsecure.messaging.getProfile(uuid, {
            profileKeyVersion: profileKeyVersionHex,
            profileKeyCredentialRequest: profileKeyCredentialRequestHex,
            userLanguages,
          });
        } else {
          throw error;
        }
      }
    } else {
      profile = await window.textsecure.messaging.getProfile(uuid, {
        profileKeyVersion: profileKeyVersionHex,
        profileKeyCredentialRequest: profileKeyCredentialRequestHex,
        userLanguages,
      });
    }

    if (profile.identityKey) {
      const identityKey = Bytes.fromBase64(profile.identityKey);
      const changed = await window.textsecure.storage.protocol.saveIdentity(
        new Address(uuid, 1),
        identityKey,
        false
      );
      if (changed) {
        // save identity will close all sessions except for .1, so we
        // must close that one manually.
        const ourUuid = window.textsecure.storage.user.getCheckedUuid();
        await window.textsecure.storage.protocol.archiveSession(
          new QualifiedAddress(ourUuid, new Address(uuid, 1))
        );
      }
    }

    const accessKey = c.get('accessKey');
    if (profile.unrestrictedUnidentifiedAccess && profile.unidentifiedAccess) {
      log.info(
        `Setting sealedSender to UNRESTRICTED for conversation ${c.idForLogging()}`
      );
      c.set({
        sealedSender: SEALED_SENDER.UNRESTRICTED,
      });
    } else if (accessKey && profile.unidentifiedAccess) {
      const haveCorrectKey = verifyAccessKey(
        Bytes.fromBase64(accessKey),
        Bytes.fromBase64(profile.unidentifiedAccess)
      );

      if (haveCorrectKey) {
        log.info(
          `Setting sealedSender to ENABLED for conversation ${c.idForLogging()}`
        );
        c.set({
          sealedSender: SEALED_SENDER.ENABLED,
        });
      } else {
        log.info(
          `Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
        );
        c.set({
          sealedSender: SEALED_SENDER.DISABLED,
        });
      }
    } else {
      log.info(
        `Setting sealedSender to DISABLED for conversation ${c.idForLogging()}`
      );
      c.set({
        sealedSender: SEALED_SENDER.DISABLED,
      });
    }

    if (profile.about) {
      const key = c.get('profileKey');
      if (key) {
        const keyBuffer = Bytes.fromBase64(key);
        const decrypted = decryptProfile(
          Bytes.fromBase64(profile.about),
          keyBuffer
        );
        c.set('about', Bytes.toString(trimForDisplay(decrypted)));
      }
    } else {
      c.unset('about');
    }

    if (profile.aboutEmoji) {
      const key = c.get('profileKey');
      if (key) {
        const keyBuffer = Bytes.fromBase64(key);
        const decrypted = decryptProfile(
          Bytes.fromBase64(profile.aboutEmoji),
          keyBuffer
        );
        c.set('aboutEmoji', Bytes.toString(trimForDisplay(decrypted)));
      }
    } else {
      c.unset('aboutEmoji');
    }

    if (profile.paymentAddress && isMe(c.attributes)) {
      window.storage.put('paymentAddress', profile.paymentAddress);
    }

    if (profile.capabilities) {
      c.set({ capabilities: profile.capabilities });
    } else {
      c.unset('capabilities');
    }

    const badges = parseBadgesFromServer(profile.badges, updatesUrl);
    if (badges.length) {
      await window.reduxActions.badges.updateOrCreate(badges);
      c.set({
        badges: badges.map(badge => ({
          id: badge.id,
          ...('expiresAt' in badge
            ? {
                expiresAt: badge.expiresAt,
                isVisible: badge.isVisible,
              }
            : {}),
        })),
      });
    } else {
      c.unset('badges');
    }

    if (profileCredentialRequestContext) {
      if (profile.credential) {
        const profileKeyCredential = handleProfileKeyCredential(
          clientZkProfileCipher,
          profileCredentialRequestContext,
          profile.credential
        );
        c.set({ profileKeyCredential });
      } else {
        c.unset('profileKeyCredential');
      }
    }
  } catch (error) {
    switch (error?.code) {
      case 401:
      case 403:
        if (
          c.get('sealedSender') === SEALED_SENDER.ENABLED ||
          c.get('sealedSender') === SEALED_SENDER.UNRESTRICTED
        ) {
          log.warn(
            `getProfile: Got 401/403 when using accessKey for ${c.idForLogging()}, removing profileKey`
          );
          c.setProfileKey(undefined);
        }
        if (c.get('sealedSender') === SEALED_SENDER.UNKNOWN) {
          log.warn(
            `getProfile: Got 401/403 when using accessKey for ${c.idForLogging()}, setting sealedSender = DISABLED`
          );
          c.set('sealedSender', SEALED_SENDER.DISABLED);
        }
        return;
      case 404:
        log.info(
          `getProfile: failed to find a profile for ${c.idForLogging()}`
        );
        c.setUnregistered();
        return;
      default:
        log.warn(
          'getProfile failure:',
          c.idForLogging(),
          error && error.stack ? error.stack : error
        );
        return;
    }
  }

  if (profile.name) {
    try {
      await c.setEncryptedProfileName(profile.name);
    } catch (error) {
      log.warn(
        'getProfile decryption failure:',
        c.idForLogging(),
        error && error.stack ? error.stack : error
      );
      await c.set({
        profileName: undefined,
        profileFamilyName: undefined,
      });
    }
  } else {
    c.set({
      profileName: undefined,
      profileFamilyName: undefined,
    });
  }

  try {
    await c.setProfileAvatar(profile.avatar);
  } catch (error) {
    if (error.code === 403 || error.code === 404) {
      log.info(`Clearing profile avatar for conversation ${c.idForLogging()}`);
      c.set({
        profileAvatar: null,
      });
    }
  }

  c.set('profileLastFetchedAt', Date.now());

  window.Signal.Data.updateConversation(c.attributes);
}
