import { Api as GramJs } from '../../../lib/gramjs';

import type {
  ApiAudio,
  ApiContact,
  ApiDice,
  ApiDocument,
  ApiFormattedText,
  ApiGame,
  ApiGiveaway,
  ApiGiveawayResults,
  ApiLocation,
  ApiMediaExtendedPreview,
  ApiMediaInvoice,
  ApiMediaTodo,
  ApiMessagePoll,
  ApiMessageStoryData,
  ApiMessageWebPage,
  ApiPaidMedia,
  ApiPhoto,
  ApiPoll,
  ApiPollAnswer,
  ApiPollResults,
  ApiStarGiftUnique,
  ApiSticker,
  ApiTodoItem,
  ApiVideo,
  ApiVoice,
  ApiWebDocument,
  ApiWebPage,
  ApiWebPageAuctionData,
  ApiWebPageStickerData,
  ApiWebPageStoryData,
  BoughtPaidMedia,
  MediaContent,
  StoryboardInfo,
} from '../../types';
import type { UniversalMessage } from './messages';

import {
  STORYBOARD_MAP_MIME,
  STORYBOARD_MIME,
  SUPPORTED_PHOTO_CONTENT_TYPES,
  SUPPORTED_VIDEO_CONTENT_TYPES,
  VIDEO_WEBM_TYPE,
} from '../../../config';
import { addTimestampEntities } from '../../../util/dates/timestamp';
import { generateWaveform } from '../../../util/generateWaveform';
import { buildCollectionByKey, pick } from '../../../util/iteratees';
import { toJSNumber } from '../../../util/numbers';
import {
  addMediaToLocalDb, addStoryToLocalDb, addWebPageMediaToLocalDb, type MediaRepairContext,
} from '../helpers/localDb';
import { serializeBytes } from '../helpers/misc';
import {
  buildApiFormattedText,
  buildApiMessageEntity,
  buildApiPhoto,
  buildApiPhotoPreviewSizes,
  buildApiPhotoSize,
  buildApiThumbnailFromPath,
  buildApiThumbnailFromStripped,
} from './common';
import { buildApiStarGift } from './gifts';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { buildStickerFromDocument, processStickerResult } from './symbols';

export function buildMessageContent(
  mtpMessage: UniversalMessage | GramJs.UpdateServiceNotification,
) {
  let content: MediaContent = {};

  if (mtpMessage.media) {
    const repairContext = 'peerId' in mtpMessage ? mtpMessage : undefined;
    content = {
      ...buildMessageMediaContent(mtpMessage.media, repairContext),
    };
  }

  const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported;

  if (mtpMessage.message && !hasUnsupportedMedia
    && !content.sticker && !content.todo && !content.contact && !content.video?.isRound) {
    const text = buildMessageTextContent(mtpMessage.message, mtpMessage.entities);
    const textWithTimestamps = addTimestampEntities(text);
    content = {
      ...content,
      text: textWithTimestamps,
    };
  }

  return content;
}

export function buildMessageTextContent(
  message: string,
  entities?: GramJs.TypeMessageEntity[],
): ApiFormattedText {
  return {
    text: message,
    ...(entities && { entities: entities.map(buildApiMessageEntity) }),
  };
}

export function buildMessageMediaContent(
  media: GramJs.TypeMessageMedia, context?: MediaRepairContext,
): MediaContent | undefined {
  addMediaToLocalDb(media, context);

  const ttlSeconds = 'ttlSeconds' in media ? media.ttlSeconds : undefined;

  const isExpiredVoice = isExpiredVoiceMessage(media);
  if (isExpiredVoice) {
    return {
      action: {
        mediaType: 'action',
        type: 'expired',
        isVoice: true,
      },
    };
  }
  const isExpiredRoundVideo = isExpiredRoundVideoMessage(media);
  if (isExpiredRoundVideo) {
    return {
      action: {
        mediaType: 'action',
        type: 'expired',
        isRoundVideo: true,
      },
    };
  }

  const voice = buildVoice(media);
  if (voice) return { voice, ttlSeconds };

  if ('round' in media && media.round) {
    const video = buildVideo(media);
    if (video) return { video, ttlSeconds };
  }

  // Other disappearing media types are not supported
  if (ttlSeconds !== undefined) {
    return undefined;
  }

  if (media instanceof GramJs.MessageMediaInvoice && media.extendedMedia instanceof GramJs.MessageExtendedMedia) {
    return buildMessageMediaContent(media.extendedMedia.media, context);
  }

  const sticker = buildSticker(media);
  if (sticker) return { sticker };

  const photo = buildPhoto(media);
  if (photo) return { photo };

  const video = buildVideo(media);
  if (video) return { video };

  const audio = buildAudio(media);
  if (audio) return { audio };

  const document = buildDocumentFromMedia(media);
  if (document) return { document };

  const contact = buildContact(media);
  if (contact) return { contact };

  const pollId = buildPollIdFromMedia(media);
  if (pollId) return { pollId };

  const todo = buildTodoFromMedia(media);
  if (todo) return { todo };

  const webPage = buildMessageWebPageFromMedia(media);
  if (webPage) return { webPage };

  const invoice = buildInvoiceFromMedia(media);
  if (invoice) return { invoice };

  const location = buildLocationFromMedia(media);
  if (location) return { location };

  const game = buildGameFromMedia(media);
  if (game) return { game };

  const dice = buildDiceFromMedia(media);
  if (dice) return { dice };

  const storyData = buildMessageStoryData(media);
  if (storyData) return { storyData };

  const giveaway = buildGiveawayFromMedia(media);
  if (giveaway) return { giveaway };

  const giveawayResults = buildGiveawayResultsFromMedia(media);
  if (giveawayResults) return { giveawayResults };

  const paidMedia = buildPaidMedia(media);
  if (paidMedia) return { paidMedia };

  return undefined;
}

function buildSticker(media: GramJs.TypeMessageMedia): ApiSticker | undefined {
  if (
    !(media instanceof GramJs.MessageMediaDocument)
    || !media.document
    || !(media.document instanceof GramJs.Document)
  ) {
    return undefined;
  }

  return buildStickerFromDocument(media.document, media.nopremium);
}

function buildPhoto(media: GramJs.TypeMessageMedia): ApiPhoto | undefined {
  if (!(media instanceof GramJs.MessageMediaPhoto) || !media.photo || !(media.photo instanceof GramJs.Photo)) {
    return undefined;
  }

  return buildApiPhoto(media.photo, media.spoiler);
}

export function buildVideoFromDocument(document: GramJs.Document, altDocuments?: GramJs.TypeDocument[], params?: {
  isSpoiler?: boolean;
  timestamp?: number;
}): ApiVideo | undefined {
  if (document instanceof GramJs.DocumentEmpty) {
    return undefined;
  }

  const altVideos = altDocuments && buildAltVideosFromDocuments(altDocuments);

  const { isSpoiler, timestamp } = params || {};

  const {
    id, mimeType, thumbs, size, videoThumbs, attributes,
  } = document;

  if (mimeType === VIDEO_WEBM_TYPE && !(self as any).isWebmSupported) {
    return undefined;
  }

  const videoAttr = attributes
    .find((a): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo);

  if (!videoAttr) {
    return undefined;
  }

  const gifAttr = attributes
    .find((a): a is GramJs.DocumentAttributeAnimated => a instanceof GramJs.DocumentAttributeAnimated);

  const hasVideoPreview = videoThumbs?.some((thumb) => thumb instanceof GramJs.VideoSize && thumb.type === 'v');
  const previewPhotoSizes = thumbs && buildApiPhotoPreviewSizes(thumbs);

  const {
    duration,
    w: width,
    h: height,
    supportsStreaming = false,
    roundMessage: isRound = false,
    nosound,
  } = videoAttr;

  const waveform = isRound ? generateWaveform(duration) : undefined;
  const storyboardInfo = altDocuments && buildStoryboardInfoFromDocuments(altDocuments);

  return {
    mediaType: 'video',
    id: String(id),
    mimeType,
    duration,
    fileName: getFilenameFromDocument(document, 'video'),
    width,
    height,
    supportsStreaming,
    isRound,
    isGif: Boolean(gifAttr),
    thumbnail: buildApiThumbnailFromStripped(thumbs),
    size: toJSNumber(size),
    isSpoiler,
    timestamp,
    hasVideoPreview,
    previewPhotoSizes,
    waveform,
    noSound: nosound,
    altVideos,
    storyboardInfo,
  };
}

export function buildAudioFromDocument(document: GramJs.Document): ApiAudio | undefined {
  if (document instanceof GramJs.DocumentEmpty) {
    return undefined;
  }

  const {
    id, mimeType, size, attributes,
  } = document;

  const audioAttributes = attributes
    .find((a: any): a is GramJs.DocumentAttributeAudio => a instanceof GramJs.DocumentAttributeAudio);

  if (!audioAttributes) {
    return undefined;
  }

  const {
    duration,
    title,
    performer,
  } = audioAttributes;

  return {
    mediaType: 'audio',
    id: String(id),
    mimeType,
    duration,
    fileName: getFilenameFromDocument(document, 'audio'),
    title,
    performer,
    size: toJSNumber(size),
  };
}

function buildVideo(media: GramJs.TypeMessageMedia): ApiVideo | undefined {
  if (
    !(media instanceof GramJs.MessageMediaDocument)
    || !(media.document instanceof GramJs.Document)
    || !media.document.mimeType.startsWith('video')
  ) {
    return undefined;
  }

  return buildVideoFromDocument(
    media.document,
    media.altDocuments,
    { isSpoiler: media.spoiler, timestamp: media.videoTimestamp },
  );
}

function buildAltVideosFromDocuments(altDocuments: GramJs.TypeDocument[], params?: {
  isSpoiler?: boolean;
}): ApiVideo[] | undefined {
  const altVideos = altDocuments.filter((d): d is GramJs.Document => (
    d instanceof GramJs.Document && d.mimeType.startsWith('video')
  )).map((alt) => buildVideoFromDocument(alt, undefined, params))
    .filter(Boolean);
  if (!altVideos.length) {
    return undefined;
  }

  return altVideos;
}

function buildStoryboardInfoFromDocuments(documents: GramJs.TypeDocument[]): StoryboardInfo | undefined {
  const storyboardMtpFile = documents.find((d): d is GramJs.Document => (
    d instanceof GramJs.Document && d.mimeType === STORYBOARD_MIME
  ));
  const storyboardMapMtpFile = documents.find((d): d is GramJs.Document => (
    d instanceof GramJs.Document && d.mimeType === STORYBOARD_MAP_MIME
  ));

  const storyboardFile = storyboardMtpFile && buildApiDocument(storyboardMtpFile);
  const storyboardMapFile = storyboardMapMtpFile && buildApiDocument(storyboardMapMtpFile);

  const sizeAttribute = storyboardMapMtpFile?.attributes.find((a): a is GramJs.DocumentAttributeImageSize => (
    a instanceof GramJs.DocumentAttributeImageSize
  ));

  const frameSize = sizeAttribute && { width: sizeAttribute.w, height: sizeAttribute.h };

  if (!storyboardFile || !storyboardMapFile || !frameSize) {
    return undefined;
  }

  return {
    storyboardFile,
    storyboardMapFile,
    frameSize,
  };
}

function buildAudio(media: GramJs.TypeMessageMedia): ApiAudio | undefined {
  if (
    !(media instanceof GramJs.MessageMediaDocument)
    || !media.document
    || !(media.document instanceof GramJs.Document)
  ) {
    return undefined;
  }

  const audioAttribute = media.document.attributes
    .find((attr: any): attr is GramJs.DocumentAttributeAudio => (
      attr instanceof GramJs.DocumentAttributeAudio
    ));

  if (!audioAttribute || audioAttribute.voice) {
    return undefined;
  }

  const thumbnailSizes = media.document.thumbs && media.document.thumbs
    .filter((thumb): thumb is GramJs.PhotoSize => thumb instanceof GramJs.PhotoSize)
    .map((thumb) => buildApiPhotoSize(thumb));

  return {
    mediaType: 'audio',
    id: String(media.document.id),
    fileName: getFilenameFromDocument(media.document, 'audio'),
    thumbnailSizes,
    size: toJSNumber(media.document.size),
    ...pick(media.document, ['mimeType']),
    ...pick(audioAttribute, ['duration', 'performer', 'title']),
  };
}

function isExpiredVoiceMessage(media: GramJs.TypeMessageMedia): boolean {
  if (!(media instanceof GramJs.MessageMediaDocument)) {
    return false;
  }
  return Boolean(!media.document && media.voice);
}

function isExpiredRoundVideoMessage(media: GramJs.TypeMessageMedia): boolean {
  if (!(media instanceof GramJs.MessageMediaDocument)) {
    return false;
  }
  return Boolean(!media.document && media.round);
}

function buildVoice(media: GramJs.TypeMessageMedia): ApiVoice | undefined {
  if (
    !(media instanceof GramJs.MessageMediaDocument)
    || !media.document
    || !(media.document instanceof GramJs.Document)
  ) {
    return undefined;
  }

  const audioAttribute = media.document.attributes
    .find((attr: any): attr is GramJs.DocumentAttributeAudio => (
      attr instanceof GramJs.DocumentAttributeAudio
    ));

  if (!audioAttribute || !audioAttribute.voice) {
    return undefined;
  }

  const { duration, waveform } = audioAttribute;

  return {
    mediaType: 'voice',
    id: String(media.document.id),
    size: toJSNumber(media.document.size),
    duration,
    waveform: waveform ? Array.from(waveform) : undefined,
  };
}

function buildDocumentFromMedia(media: GramJs.TypeMessageMedia) {
  if (!(media instanceof GramJs.MessageMediaDocument) || !media.document) {
    return undefined;
  }

  return buildApiDocument(media.document);
}

export function buildApiDocument(document: GramJs.TypeDocument): ApiDocument | undefined {
  if (!(document instanceof GramJs.Document)) {
    return undefined;
  }

  const {
    id, size, mimeType, date, thumbs, attributes,
  } = document;

  const photoSize = thumbs && thumbs.find((s): s is GramJs.PhotoSize => s instanceof GramJs.PhotoSize);
  let thumbnail = thumbs && buildApiThumbnailFromStripped(thumbs);
  if (!thumbnail && thumbs && photoSize) {
    const photoPath = thumbs.find((s): s is GramJs.PhotoPathSize => s instanceof GramJs.PhotoPathSize);
    if (photoPath) {
      thumbnail = buildApiThumbnailFromPath(photoPath, photoSize);
    }
  }
  const previewPhotoSizes = thumbs && buildApiPhotoPreviewSizes(thumbs);

  let innerMediaType: ApiDocument['innerMediaType'] | undefined;
  let mediaSize: ApiDocument['mediaSize'] | undefined;
  if (photoSize) {
    mediaSize = {
      width: photoSize.w,
      height: photoSize.h,
    };

    if (SUPPORTED_PHOTO_CONTENT_TYPES.has(mimeType)) {
      innerMediaType = 'photo';

      const imageAttribute = attributes
        .find((a): a is GramJs.DocumentAttributeImageSize => a instanceof GramJs.DocumentAttributeImageSize);

      if (imageAttribute) {
        const { w: width, h: height } = imageAttribute;
        mediaSize = {
          width,
          height,
          fromDocumentAttribute: true,
        };
      }
    } else if (SUPPORTED_VIDEO_CONTENT_TYPES.has(mimeType)) {
      innerMediaType = 'video';
      const videoAttribute = attributes
        .find((a: any): a is GramJs.DocumentAttributeVideo => a instanceof GramJs.DocumentAttributeVideo);

      if (videoAttribute) {
        const { w: width, h: height } = videoAttribute;
        mediaSize = {
          width,
          height,
        };
      }
    }
  }

  return {
    mediaType: 'document',
    id: String(id),
    size: toJSNumber(size),
    mimeType,
    timestamp: date,
    fileName: getFilenameFromDocument(document),
    thumbnail,
    innerMediaType,
    mediaSize,
    previewPhotoSizes,
  };
}

function buildContact(media: GramJs.TypeMessageMedia): ApiContact | undefined {
  if (!(media instanceof GramJs.MessageMediaContact)) {
    return undefined;
  }

  const {
    firstName, lastName, phoneNumber, userId,
  } = media;

  return {
    mediaType: 'contact',
    firstName,
    lastName,
    phoneNumber,
    userId: buildApiPeerId(userId, 'user'),
  };
}

function buildPollIdFromMedia(media: GramJs.TypeMessageMedia): string | undefined {
  if (!(media instanceof GramJs.MessageMediaPoll)) {
    return undefined;
  }

  return media.poll.id.toString();
}

export function buildMessagePollFromMedia(media: GramJs.TypeMessageMedia): ApiMessagePoll | undefined {
  if (!(media instanceof GramJs.MessageMediaPoll)) {
    return undefined;
  }

  return buildMessagePoll(media);
}

function buildTodoFromMedia(media: GramJs.TypeMessageMedia): ApiMediaTodo | undefined {
  if (!(media instanceof GramJs.MessageMediaToDo)) {
    return undefined;
  }

  return buildTodo(media.todo, media.completions);
}

function buildInvoiceFromMedia(media: GramJs.TypeMessageMedia): ApiMediaInvoice | undefined {
  if (!(media instanceof GramJs.MessageMediaInvoice)) {
    return undefined;
  }

  return buildMediaInvoice(media);
}

function buildLocationFromMedia(media: GramJs.TypeMessageMedia): ApiLocation | undefined {
  if (media instanceof GramJs.MessageMediaGeo) {
    return buildGeo(media);
  }

  if (media instanceof GramJs.MessageMediaVenue) {
    return buildVenue(media);
  }

  if (media instanceof GramJs.MessageMediaGeoLive) {
    return buildGeoLive(media);
  }

  return undefined;
}

function buildGeo(media: GramJs.MessageMediaGeo): ApiLocation | undefined {
  const point = buildGeoPoint(media.geo);
  return point && { mediaType: 'geo', geo: point };
}

function buildVenue(media: GramJs.MessageMediaVenue): ApiLocation | undefined {
  const {
    geo, title, provider, address, venueId, venueType,
  } = media;
  const point = buildGeoPoint(geo);
  return point && {
    mediaType: 'venue',
    geo: point,
    title,
    provider,
    address,
    venueId,
    venueType,
  };
}

function buildGeoLive(media: GramJs.MessageMediaGeoLive): ApiLocation | undefined {
  const { geo, period, heading } = media;
  const point = buildGeoPoint(geo);
  return point && {
    mediaType: 'geoLive',
    geo: point,
    period,
    heading,
  };
}

export function buildGeoPoint(geo: GramJs.TypeGeoPoint): ApiLocation['geo'] | undefined {
  if (geo instanceof GramJs.GeoPointEmpty) return undefined;
  const {
    long, lat, accuracyRadius, accessHash,
  } = geo;
  return {
    long,
    lat,
    accessHash: accessHash.toString(),
    accuracyRadius,
  };
}

function buildGameFromMedia(media: GramJs.TypeMessageMedia): ApiGame | undefined {
  if (!(media instanceof GramJs.MessageMediaGame)) {
    return undefined;
  }

  return buildGame(media);
}

function buildGame(media: GramJs.MessageMediaGame): ApiGame | undefined {
  const {
    id, accessHash, shortName, title, description, photo: apiPhoto, document: apiDocument,
  } = media.game;

  const photo = apiPhoto instanceof GramJs.Photo ? buildApiPhoto(apiPhoto) : undefined;
  const document = apiDocument instanceof GramJs.Document ? buildApiDocument(apiDocument) : undefined;

  return {
    mediaType: 'game',
    id: id.toString(),
    accessHash: accessHash.toString(),
    shortName,
    title,
    description,
    photo,
    document,
  };
}

function buildDiceFromMedia(media: GramJs.TypeMessageMedia): ApiDice | undefined {
  if (!(media instanceof GramJs.MessageMediaDice)) {
    return undefined;
  }

  return buildDice(media);
}

function buildDice(media: GramJs.MessageMediaDice): ApiDice | undefined {
  const { value, emoticon } = media;
  return {
    mediaType: 'dice',
    value,
    emoticon,
  };
}

function buildGiveawayFromMedia(media: GramJs.TypeMessageMedia): ApiGiveaway | undefined {
  if (!(media instanceof GramJs.MessageMediaGiveaway)) {
    return undefined;
  }

  return buildGiveaway(media);
}

function buildGiveaway(media: GramJs.MessageMediaGiveaway): ApiGiveaway | undefined {
  const {
    channels, months, stars, quantity, untilDate, countriesIso2, onlyNewSubscribers, prizeDescription,
  } = media;

  const channelIds = channels.map((channel) => buildApiPeerId(channel, 'channel'));

  return {
    mediaType: 'giveaway',
    channelIds,
    months,
    stars: toJSNumber(stars),
    quantity,
    untilDate,
    countries: countriesIso2,
    isOnlyForNewSubscribers: onlyNewSubscribers,
    prizeDescription,
  };
}

function buildGiveawayResultsFromMedia(media: GramJs.TypeMessageMedia): ApiGiveawayResults | undefined {
  if (!(media instanceof GramJs.MessageMediaGiveawayResults)) {
    return undefined;
  }

  return buildGiveawayResults(media);
}

function buildGiveawayResults(media: GramJs.MessageMediaGiveawayResults): ApiGiveawayResults | undefined {
  const {
    months, untilDate, onlyNewSubscribers, launchMsgId, unclaimedCount, winners, winnersCount,
    additionalPeersCount, prizeDescription, refunded, channelId,
  } = media;

  const winnerIds = winners.map((winner) => buildApiPeerId(winner, 'user'));

  return {
    mediaType: 'giveawayResults',
    months,
    untilDate,
    isOnlyForNewSubscribers: onlyNewSubscribers,
    launchMessageId: launchMsgId,
    channelId: buildApiPeerId(channelId, 'channel'),
    unclaimedCount,
    additionalPeersCount,
    isRefunded: refunded,
    prizeDescription,
    winnerIds,
    winnersCount,
  };
}

export function buildMessageStoryData(media: GramJs.TypeMessageMedia): ApiMessageStoryData | undefined {
  if (!(media instanceof GramJs.MessageMediaStory)) {
    return undefined;
  }

  const peerId = getApiChatIdFromMtpPeer(media.peer);

  return {
    mediaType: 'storyData',
    id: media.id,
    peerId,
    ...(media.viaMention && { isMention: true }),
  };
}

export function buildMessagePoll(media: GramJs.MessageMediaPoll): ApiMessagePoll {
  const { poll, results, attachedMedia } = media;

  return {
    mediaType: 'poll',
    summary: buildPoll(poll),
    results: buildPollResults(results),
    attachedMedia: attachedMedia ? buildMessageMediaContent(attachedMedia) : undefined,
  };
}

export function buildPollAnswer(answer: GramJs.TypePollAnswer): ApiPollAnswer | undefined {
  if (!(answer instanceof GramJs.PollAnswer)) return undefined;
  const { text, option, media, addedBy, date } = answer;

  return {
    text: buildApiFormattedText(text),
    option: serializeBytes(option),
    media: media ? buildMessageMediaContent(media) : undefined,
    addedByPeerId: addedBy ? getApiChatIdFromMtpPeer(addedBy) : undefined,
    date,
  };
}

export function buildPoll(poll: GramJs.Poll): ApiPoll {
  const {
    id, closed, publicVoters, multipleChoice, quiz, closePeriod, closeDate, answers, question, creator,
    hideResultsUntilClose, revotingDisabled, shuffleAnswers, openAnswers, hash,
  } = poll;
  const apiAnswers = answers.map(buildPollAnswer).filter(Boolean);

  return {
    id: id.toString(),
    isClosed: closed,
    isPublic: publicVoters,
    isMultipleChoice: multipleChoice,
    isQuiz: quiz,
    closePeriod,
    closeDate,
    isCreator: creator,
    shouldHideResultsUntilClose: hideResultsUntilClose,
    isRevoteDisabled: revotingDisabled,
    shouldShuffleAnswers: shuffleAnswers,
    question: buildApiFormattedText(question),
    answers: apiAnswers,
    hash: hash.toString(),
    canAddAnswers: openAnswers,
  };
}

export function buildTodoItem(item: GramJs.TodoItem): ApiTodoItem {
  return {
    id: item.id,
    title: buildApiFormattedText(item.title),
  };
}

export function buildTodo(todo: GramJs.TodoList, completions?: GramJs.TodoCompletion[]): ApiMediaTodo {
  const { title, list: items } = todo;

  const todoItems = items.map(buildTodoItem);

  const todoCompletions = completions?.map((completion) => ({
    itemId: completion.id,
    completedBy: getApiChatIdFromMtpPeer(completion.completedBy),
    completedAt: completion.date,
  }));

  return {
    mediaType: 'todo',
    todo: {
      title: buildApiFormattedText(title),
      items: todoItems,
      othersCanAppend: todo.othersCanAppend,
      othersCanComplete: todo.othersCanComplete,
    },
    completions: todoCompletions,
  };
}

export function buildMediaInvoice(media: GramJs.MessageMediaInvoice): ApiMediaInvoice {
  const {
    description, title, photo, test, totalAmount, currency, receiptMsgId, extendedMedia,
  } = media;

  const preview = extendedMedia instanceof GramJs.MessageExtendedMediaPreview
    ? buildApiMessageExtendedMediaPreview(extendedMedia) : undefined;

  return {
    mediaType: 'invoice',
    title,
    description,
    photo: buildApiWebDocument(photo),
    receiptMessageId: receiptMsgId,
    amount: toJSNumber(totalAmount),
    currency,
    isTest: test,
    extendedMedia: preview,
  };
}

export function buildPollResults(pollResults: GramJs.PollResults): ApiPollResults {
  const {
    results: rawResults, totalVoters, recentVoters, solution, solutionEntities: entities, min, solutionMedia,
  } = pollResults;
  const results = rawResults?.map(({
    option, chosen, correct, voters, recentVoters: recentAnswerVoters,
  }) => ({
    isChosen: chosen,
    isCorrect: correct,
    option: serializeBytes(option),
    votersCount: voters ?? 0,
    recentVoterIds: recentAnswerVoters?.map((peer) => getApiChatIdFromMtpPeer(peer)),
  }));

  if (solutionMedia) {
    addMediaToLocalDb(solutionMedia);
  }

  return {
    isMin: min,
    totalVoters,
    recentVoterIds: recentVoters?.map((peer) => getApiChatIdFromMtpPeer(peer)),
    resultByOption: results && buildCollectionByKey(results, 'option'),
    solution,
    solutionEntities: entities?.map(buildApiMessageEntity),
    solutionMedia: solutionMedia ? buildMessageMediaContent(solutionMedia) : undefined,
  };
}

export function buildMessageWebPageFromMedia(media: GramJs.TypeMessageMedia): ApiMessageWebPage | undefined {
  if (!(media instanceof GramJs.MessageMediaWebPage) || media.webpage instanceof GramJs.WebPageNotModified) {
    return undefined;
  }
  const {
    webpage, forceLargeMedia, forceSmallMedia, safe,
  } = media;

  return {
    id: webpage.id.toString(),
    isSafe: safe,
    mediaSize: forceSmallMedia ? 'small' : forceLargeMedia ? 'large' : undefined,
  };
}

export function buildWebPageFromMedia(media: GramJs.TypeMessageMedia): ApiWebPage | undefined {
  if (!(media instanceof GramJs.MessageMediaWebPage)) {
    return undefined;
  }
  const {
    webpage,
  } = media;

  return buildWebPage(webpage);
}

export function buildWebPage(webPage: GramJs.TypeWebPage): ApiWebPage | undefined {
  addWebPageMediaToLocalDb(webPage);

  if (webPage instanceof GramJs.WebPageEmpty) {
    return {
      mediaType: 'webpage',
      webpageType: 'empty',
      id: webPage.id.toString(),
      url: webPage.url,
    };
  }

  if (webPage instanceof GramJs.WebPagePending) {
    return {
      mediaType: 'webpage',
      webpageType: 'pending',
      id: webPage.id.toString(),
      url: webPage.url,
    };
  }

  if (webPage instanceof GramJs.WebPage) {
    const {
      id, photo, document, attributes,
    } = webPage;

    let video;
    let audio;
    if (document instanceof GramJs.Document && document.mimeType.startsWith('video/')) {
      video = buildVideoFromDocument(document);
    }
    if (document instanceof GramJs.Document && document.mimeType.startsWith('audio/')) {
      audio = buildAudioFromDocument(document);
    }
    let story: ApiWebPageStoryData | undefined;
    let gift: ApiStarGiftUnique | undefined;
    let auction: ApiWebPageAuctionData | undefined;
    let stickers: ApiWebPageStickerData | undefined;
    const attributeStory = attributes
      ?.find((a): a is GramJs.WebPageAttributeStory => a instanceof GramJs.WebPageAttributeStory);
    const attributeGift = attributes
      ?.find((a): a is GramJs.WebPageAttributeUniqueStarGift => a instanceof GramJs.WebPageAttributeUniqueStarGift);
    const attributeAuction = attributes
      ?.find((a): a is GramJs.WebPageAttributeStarGiftAuction => (
        a instanceof GramJs.WebPageAttributeStarGiftAuction
      ));
    if (attributeStory) {
      const peerId = getApiChatIdFromMtpPeer(attributeStory.peer);
      story = {
        id: attributeStory.id,
        peerId,
      };

      if (attributeStory.story instanceof GramJs.StoryItem) {
        addStoryToLocalDb(attributeStory.story, peerId);
      }
    }
    if (attributeGift) {
      const starGift = buildApiStarGift(attributeGift.gift);
      gift = starGift.type === 'starGiftUnique' ? starGift : undefined;
    }
    if (attributeAuction) {
      const starGift = buildApiStarGift(attributeAuction.gift);
      if (starGift.type === 'starGift') {
        auction = {
          gift: starGift,
          endDate: attributeAuction.endDate,
        };
      }
    }
    const attributeStickers = attributes?.find((a): a is GramJs.WebPageAttributeStickerSet => (
      a instanceof GramJs.WebPageAttributeStickerSet
    ));
    if (attributeStickers) {
      stickers = {
        documents: processStickerResult(attributeStickers.stickers),
        isEmoji: attributeStickers.emojis,
        isWithTextColor: attributeStickers.textColor,
      };
    }

    const attributeAiTone = attributes?.find((a): a is GramJs.WebPageAttributeAiComposeTone => (
      a instanceof GramJs.WebPageAttributeAiComposeTone
    ));

    return {
      mediaType: 'webpage',
      webpageType: 'full',
      id: id.toString(),
      ...pick(webPage, [
        'url',
        'displayUrl',
        'type',
        'siteName',
        'title',
        'description',
        'duration',
        'hasLargeMedia',
      ]),
      photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined,
      document: !video && !audio && document ? buildApiDocument(document) : undefined,
      video,
      audio,
      story,
      gift,
      auction,
      stickers,
      aiComposeToneEmojiId: attributeAiTone?.emojiId.toString(),
    };
  }

  return undefined;
}

function buildPaidMedia(media: GramJs.TypeMessageMedia): ApiPaidMedia | undefined {
  if (!(media instanceof GramJs.MessageMediaPaidMedia)) {
    return undefined;
  }

  const { starsAmount, extendedMedia } = media;

  const isBought = extendedMedia[0] instanceof GramJs.MessageExtendedMedia;

  if (isBought) {
    return {
      mediaType: 'paidMedia',
      starsAmount: toJSNumber(starsAmount),
      isBought,
      extendedMedia: buildBoughtMediaContent(extendedMedia)!,
    };
  }

  return {
    mediaType: 'paidMedia',
    starsAmount: toJSNumber(starsAmount),
    extendedMedia: extendedMedia
      .filter((paidMedia): paidMedia is GramJs.MessageExtendedMediaPreview => (
        paidMedia instanceof GramJs.MessageExtendedMediaPreview
      ))
      .map((paidMedia) => buildApiMessageExtendedMediaPreview(paidMedia)),
  };
}

function getFilenameFromDocument(document: GramJs.Document, defaultBase = 'file') {
  const { mimeType, attributes } = document;
  const filenameAttribute = attributes
    .find((a: any): a is GramJs.DocumentAttributeFilename => a instanceof GramJs.DocumentAttributeFilename);

  if (filenameAttribute) {
    return filenameAttribute.fileName;
  }

  const extension = mimeType.split('/')[1];

  return `${defaultBase}${String(document.id)}.${extension}`;
}

export function buildApiMessageExtendedMediaPreview(
  preview: GramJs.MessageExtendedMediaPreview,
): ApiMediaExtendedPreview {
  const {
    w, h, thumb, videoDuration,
  } = preview;

  return {
    mediaType: 'extendedMediaPreview',
    width: w,
    height: h,
    duration: videoDuration,
    thumbnail: thumb ? buildApiThumbnailFromStripped([thumb]) : undefined,
  };
}

export function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDocument | undefined {
  if (!document) return undefined;

  const {
    url, size, mimeType,
  } = document;
  const accessHash = document instanceof GramJs.WebDocument ? document.accessHash.toString() : undefined;
  const sizeAttr = document.attributes.find((attr): attr is GramJs.DocumentAttributeImageSize => (
    attr instanceof GramJs.DocumentAttributeImageSize
  ));
  const dimensions = sizeAttr && { width: sizeAttr.w, height: sizeAttr.h };

  return {
    mediaType: 'webDocument',
    url,
    accessHash,
    size,
    mimeType,
    dimensions,
  };
}

export function buildBoughtMediaContent(
  media: GramJs.TypeMessageExtendedMedia[],
): BoughtPaidMedia[] | undefined {
  const boughtMedia = media
    .filter((m): m is GramJs.MessageExtendedMedia => m instanceof GramJs.MessageExtendedMedia)
    .map((m) => buildMessageMediaContent(m.media))
    .filter(Boolean);

  if (!boughtMedia.length) {
    return undefined;
  }

  return boughtMedia;
}
