/**
 * Get history REST API module.
 *
 * @internal
 */

import { TransportResponse } from '../../types/transport-response';
import { ICryptoModule } from '../../interfaces/crypto-module';
import { AbstractRequest } from '../../components/request';
import RequestOperation from '../../constants/operations';
import { KeySet, Payload, Query } from '../../types/api';
import * as History from '../../types/api/history';
import { encodeString } from '../../utils';

// --------------------------------------------------------
// ---------------------- Defaults ------------------------
// --------------------------------------------------------
// region Defaults

/**
 * Whether verbose logging enabled or not.
 */
const LOG_VERBOSITY = false;

/**
 * Whether associated message metadata should be returned or not.
 */
const INCLUDE_METADATA = false;

/**
 * Whether timetokens should be returned as strings by default or not.
 */
const STRINGIFY_TIMETOKENS = false;

/**
 * Default and maximum number of messages which should be returned.
 */
const MESSAGES_COUNT = 100;
// endregion

// --------------------------------------------------------
// ------------------------ Types -------------------------
// --------------------------------------------------------
// region Types

/**
 * Request configuration parameters.
 */
type RequestParameters = History.GetHistoryParameters & {
  /**
   * PubNub REST API access key set.
   */
  keySet: KeySet;

  /**
   * Published data encryption module.
   */
  crypto?: ICryptoModule;

  /**
   * Whether verbose logging enabled or not.
   *
   * @default `false`
   */
  logVerbosity?: boolean;
};

/**
 * Service success response.
 */
type ServiceResponse = [
  /**
   * List of previously published messages.
   */
  {
    /**
     * Message payload (decrypted).
     */
    message: Payload;

    /**
     * When message has been received by PubNub service.
     */
    timetoken: string | number;

    /**
     * Additional data which has been published along with message to be used with real-time
     * events filter expression.
     */
    meta?: Payload;
  }[],

  /**
   * Received messages timeline start.
   */
  string | number,

  /**
   * Received messages timeline end.
   */
  string | number,
];
// endregion

/**
 * Get single channel messages request.
 *
 * @internal
 */
export class GetHistoryRequest extends AbstractRequest<History.GetHistoryResponse, ServiceResponse> {
  constructor(private readonly parameters: RequestParameters) {
    super();

    // Apply defaults.
    if (parameters.count) parameters.count = Math.min(parameters.count, MESSAGES_COUNT);
    else parameters.count = MESSAGES_COUNT;

    parameters.stringifiedTimeToken ??= STRINGIFY_TIMETOKENS;
    parameters.includeMeta ??= INCLUDE_METADATA;
    parameters.logVerbosity ??= LOG_VERBOSITY;
  }

  operation(): RequestOperation {
    return RequestOperation.PNHistoryOperation;
  }

  validate(): string | undefined {
    if (!this.parameters.keySet.subscribeKey) return 'Missing Subscribe Key';
    if (!this.parameters.channel) return 'Missing channel';
  }

  async parse(response: TransportResponse): Promise<History.GetHistoryResponse> {
    const serviceResponse = this.deserializeResponse(response);
    const messages = serviceResponse[0];
    const startTimeToken = serviceResponse[1];
    const endTimeToken = serviceResponse[2];

    // Handle malformed get history response.
    if (!Array.isArray(messages)) return { messages: [], startTimeToken, endTimeToken };

    return {
      messages: messages.map((payload) => {
        const processedPayload = this.processPayload(payload.message);
        const item: History.GetHistoryResponse['messages'][number] = {
          entry: processedPayload.payload,
          timetoken: payload.timetoken,
        };

        if (processedPayload.error) item.error = processedPayload.error;
        if (payload.meta) item.meta = payload.meta;

        return item;
      }),
      startTimeToken,
      endTimeToken,
    };
  }

  protected get path(): string {
    const {
      keySet: { subscribeKey },
      channel,
    } = this.parameters;

    return `/v2/history/sub-key/${subscribeKey}/channel/${encodeString(channel)}`;
  }

  protected get queryParameters(): Query {
    const { start, end, reverse, count, stringifiedTimeToken, includeMeta } = this.parameters;

    return {
      count: count!,
      include_token: 'true',
      ...(start ? { start } : {}),
      ...(end ? { end } : {}),
      ...(stringifiedTimeToken! ? { string_message_token: 'true' } : {}),
      ...(reverse !== undefined && reverse !== null ? { reverse: reverse.toString() } : {}),
      ...(includeMeta! ? { include_meta: 'true' } : {}),
    };
  }

  private processPayload(payload: Payload): { payload: Payload; error?: string } {
    const { crypto, logVerbosity } = this.parameters;
    if (!crypto || typeof payload !== 'string') return { payload };

    let decryptedPayload: string;
    let error: string | undefined;

    try {
      const decryptedData = crypto.decrypt(payload);
      decryptedPayload =
        decryptedData instanceof ArrayBuffer
          ? JSON.parse(GetHistoryRequest.decoder.decode(decryptedData))
          : decryptedData;
    } catch (err) {
      if (logVerbosity!) console.log(`decryption error`, (err as Error).message);
      decryptedPayload = payload;
      error = `Error while decrypting message content: ${(err as Error).message}`;
    }

    return {
      payload: decryptedPayload,
      error,
    };
  }
}
