/* eslint-disable no-restricted-syntax */
import { isFinite, isNil, isNumber, sample } from 'lodash';
import pRetry from 'p-retry';
import { Snode } from '../../../data/types';
import { UserUtils } from '../../utils';
import { EmptySwarmError } from '../../utils/errors';
import { SeedNodeAPI } from '../seed_node_api';
import { GetExpiriesFromNodeSubRequest, fakeHash } from './SnodeRequestTypes';
import { doSnodeBatchRequest } from './batchRequest';
import { GetNetworkTime } from './getNetworkTime';
import { getSwarmFor } from './snodePool';
import { SnodeSignature } from './snodeSignatures';
import { GetExpiriesResultsContent } from './types';

export type GetExpiriesRequestResponseResults = Record<string, number>;

export async function processGetExpiriesRequestResponse(
  _targetNode: Snode,
  expiries: GetExpiriesResultsContent,
  messageHashes: Array<string>
): Promise<GetExpiriesRequestResponseResults> {
  if (isNil(expiries)) {
    throw Error(
      `[processGetExpiriesRequestResponse] Expiries are nul/undefined! ${JSON.stringify(
        messageHashes
      )}`
    );
  }

  const results: GetExpiriesRequestResponseResults = {};
  // Note: we iterate over the hash we've requested and not the one we received,
  // because a message which expired already is not in the result at all (and we need to force it to be expired)
  for (const messageHash of messageHashes) {
    const expiryMs = expiries[messageHash];

    if (expiries[messageHash] && isNumber(expiryMs) && isFinite(expiryMs)) {
      results[messageHash] = expiryMs;
    } // not adding the Date.now() fallback here as it is done in the caller of this function
  }

  return results;
}

async function getExpiriesFromNodes(
  targetNode: Snode,
  expireRequest: GetExpiriesFromNodeSubRequest
) {
  try {
    const result = await doSnodeBatchRequest(
      [expireRequest],
      targetNode,
      4000,
      expireRequest.params.pubkey,
      'batch'
    );

    if (!result || result.length !== 1) {
      throw Error(
        `There was an issue with the results. sessionRpc ${targetNode.ip}:${
          targetNode.port
        } expireRequest ${JSON.stringify(expireRequest)}`
      );
    }

    // TODOLATER make sure that this code still works once disappearing messages is merged
    // do a basic check to know if we have something kind of looking right (status 200 should always be there for a retrieve)
    const firstResult = result[0];

    if (firstResult.code !== 200) {
      throw Error(`getExpiriesFromNodes result is not 200 but ${firstResult.code}`);
    }

    // expirationResults is a record of {messageHash: currentExpiry}
    const expirationResults = await processGetExpiriesRequestResponse(
      targetNode,
      firstResult.body.expiries as GetExpiriesResultsContent,
      expireRequest.params.messages
    );

    // Note: even if expirationResults is empty we need to process the results.
    // The status code is 200, so if the results is empty, it means all those messages already expired.

    // Note: a hash which already expired on the server is not going to be returned. So we force it's fetchedExpiry to be now() to make it expire asap
    const expiriesWithForcedExpiried = expireRequest.params.messages.map(messageHash => ({
      messageHash,
      fetchedExpiry: expirationResults?.[messageHash] || Date.now(),
    }));

    return expiriesWithForcedExpiried;
  } catch (err) {
    // NOTE batch requests have their own retry logic which includes abort errors that will break our retry logic so we need to catch them and throw regular errors
    if (err instanceof pRetry.AbortError) {
      throw Error(err.message);
    }

    throw err;
  }
}

export type GetExpiriesFromSnodeProps = {
  messageHashes: Array<string>;
};

export async function buildGetExpiriesRequest({
  messageHashes,
}: GetExpiriesFromSnodeProps): Promise<GetExpiriesFromNodeSubRequest | null> {
  const timestamp = GetNetworkTime.getNowWithNetworkOffset();

  const ourPubKey = UserUtils.getOurPubKeyStrFromCache();
  if (!ourPubKey) {
    window.log.error('[buildGetExpiriesRequest] No pubkey found', messageHashes);
    return null;
  }

  const signResult = await SnodeSignature.generateGetExpiriesSignature({
    timestamp,
    messageHashes,
  });

  if (!signResult) {
    window.log.error(
      `[buildGetExpiriesRequest] SnodeSignature.generateUpdateExpirySignature returned an empty result ${messageHashes}`
    );
    return null;
  }

  const getExpiriesParams: GetExpiriesFromNodeSubRequest = {
    method: 'get_expiries',
    params: {
      pubkey: ourPubKey,
      pubkey_ed25519: signResult.pubkey_ed25519.toUpperCase(),
      messages: messageHashes,
      timestamp,
      signature: signResult?.signature,
    },
  };

  return getExpiriesParams;
}

/**
 * Sends an 'get_expiries' request which retrieves the current expiry timestamps of the given messages.
 *
 * The returned TTLs should be assigned to the given disappearing messages.
 * @param messageHashes the hashes of the messages we want the current expiries for
 * @param timestamp the time (ms) the request was initiated, must be within ±60s of the current time so using the server time is recommended.
 * @returns an arrray of the expiry timestamps (TTL) for the given messages
 */
export async function getExpiriesFromSnode({ messageHashes }: GetExpiriesFromSnodeProps) {
  // FIXME There is a bug in the snode code that requires at least 2 messages to be requested. Will be fixed in next storage server release
  if (messageHashes.length === 1) {
    messageHashes.push(fakeHash);
  }

  const ourPubKey = UserUtils.getOurPubKeyStrFromCache();
  if (!ourPubKey) {
    window.log.error('[getExpiriesFromSnode] No pubkey found', messageHashes);
    return [];
  }

  let snode: Snode | undefined;

  try {
    const expireRequestParams = await buildGetExpiriesRequest({ messageHashes });
    if (!expireRequestParams) {
      throw new Error(`Failed to build get_expiries request ${JSON.stringify({ messageHashes })}`);
    }

    const fetchedExpiries = await pRetry(
      async () => {
        const swarm = await getSwarmFor(ourPubKey);
        snode = sample(swarm);
        if (!snode) {
          throw new EmptySwarmError(ourPubKey, 'Ran out of swarm nodes to query');
        }
        return getExpiriesFromNodes(snode, expireRequestParams);
      },
      {
        retries: 3,
        factor: 2,
        minTimeout: SeedNodeAPI.getMinTimeout(),
        onFailedAttempt: e => {
          window?.log?.warn(
            `[getExpiriesFromSnode] get expiries from snode attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left... Error: ${e.message}`
          );
        },
      }
    );

    return fetchedExpiries;
  } catch (e) {
    const snodeStr = snode ? `${snode.ip}:${snode.port}` : 'null';
    window?.log?.warn(
      `[getExpiriesFromSnode] ${e.code ? `${e.code} ` : ''}${
        e.message || e
      } by ${ourPubKey} for ${messageHashes} via snode:${snodeStr}`
    );
    throw e;
  }
}
