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

import type {
  ApiError,
  ApiInputPrivacyRules,
  ApiPeer,
  ApiPeerStories,
  ApiReaction,
  ApiStealthMode,
  ApiStoryAlbum,
  ApiTypeStory,
} from '../../types';

import { MESSAGE_ID_REQUIRED_ERROR } from '../../../config';
import { buildCollectionByCallback } from '../../../util/iteratees';
import { STORY_LIST_LIMIT } from '../../../limits';
import { buildApiReportResult } from '../apiBuilders/messages';
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import {
  buildApiPeerStories,
  buildApiStealthMode,
  buildApiStory,
  buildApiStoryAlbum,
  buildApiStoryView,
  buildApiStoryViews,
} from '../apiBuilders/stories';
import {
  buildInputPeer,
  buildInputPrivacyRules,
  buildInputReaction,
  DEFAULT_PRIMITIVES,
} from '../gramjsBuilders';
import { addStoryToLocalDb } from '../helpers/localDb';
import { deserializeBytes } from '../helpers/misc';
import { dispatchErrorUpdate, invokeRequest } from './client';

export async function fetchAllStories({
  stateHash,
  isFirstRequest = false,
  isHidden = false,
}: {
  isFirstRequest?: boolean;
  stateHash?: string;
  isHidden?: boolean;
}): Promise<
  undefined
  | { state: string; stealthMode: ApiStealthMode }
  | {
    peerStories: Record<string, ApiPeerStories>;
    hasMore?: true;
    state: string;
    stealthMode: ApiStealthMode;
  }> {
  const params: ConstructorParameters<typeof GramJs.stories.GetAllStories>[0] = isFirstRequest
    ? (isHidden ? { hidden: true } : {})
    : { state: stateHash, next: true, ...(isHidden && { hidden: true }) };
  const result = await invokeRequest(new GramJs.stories.GetAllStories(params));

  if (!result) {
    return undefined;
  }

  if (result instanceof GramJs.stories.AllStoriesNotModified) {
    return {
      state: result.state,
      stealthMode: buildApiStealthMode(result.stealthMode),
    };
  }

  const allUserStories = result.peerStories.reduce<Record<string, ApiPeerStories>>((acc, peerStories) => {
    const peerId = getApiChatIdFromMtpPeer(peerStories.peer);
    const stories = buildApiPeerStories(peerStories);
    const { profileIds, orderedIds, lastUpdatedAt } = Object.values(stories).reduce<
      {
        profileIds: number[];
        orderedIds: number[];
        lastUpdatedAt?: number;
      }
    >((dataAcc, story) => {
      if ('isInProfile' in story && story.isInProfile) {
        dataAcc.profileIds.push(story.id);
      }
      if (!('isDeleted' in story)) {
        dataAcc.orderedIds.push(story.id);
        dataAcc.lastUpdatedAt = Math.max(story.date, dataAcc.lastUpdatedAt || 0);
      }

      return dataAcc;
    }, {
      profileIds: [],
      orderedIds: [],
      lastUpdatedAt: undefined,
    });

    if (orderedIds.length === 0) {
      return acc;
    }

    acc[peerId] = {
      byId: stories,
      orderedIds,
      profileIds,
      lastUpdatedAt,
      lastReadId: peerStories.maxReadId,
    };

    return acc;
  }, {});

  // Add after building stories to avoid overwriting repair info
  result.peerStories.forEach((peerStories) => (
    peerStories.stories.forEach((story) => addStoryToLocalDb(story, getApiChatIdFromMtpPeer(peerStories.peer)))
  ));

  return {
    peerStories: allUserStories,
    hasMore: result.hasMore,
    state: result.state,
    stealthMode: buildApiStealthMode(result.stealthMode),
  };
}

export async function fetchPeerStories({
  peer,
}: {
  peer: ApiPeer;
}) {
  const result = await invokeRequest(new GramJs.stories.GetPeerStories({
    peer: buildInputPeer(peer.id, peer.accessHash),
  }));

  if (!result) {
    return undefined;
  }

  const stories = buildCollectionByCallback(result.stories.stories, (story) => (
    [story.id, buildApiStory(peer.id, story)]
  ));

  // Add after building stories to avoid overwriting repair info
  result.stories.stories.forEach((story) => addStoryToLocalDb(story, peer.id));

  return {
    stories,
    lastReadStoryId: result.stories.maxReadId,
  };
}

export function fetchPeerProfileStories({
  peer,
  offsetId = DEFAULT_PRIMITIVES.INT,
}: {
  peer: ApiPeer;
  offsetId?: number;
}) {
  return fetchCommonStoriesRequest({
    method: new GramJs.stories.GetPinnedStories({
      peer: buildInputPeer(peer.id, peer.accessHash),
      offsetId,
      limit: STORY_LIST_LIMIT,
    }),
    peerId: peer.id,
  });
}

export function fetchStoriesArchive({
  peer,
  offsetId = DEFAULT_PRIMITIVES.INT,
}: {
  peer: ApiPeer;
  offsetId?: number;
}) {
  return fetchCommonStoriesRequest({
    method: new GramJs.stories.GetStoriesArchive({
      peer: peer && buildInputPeer(peer.id, peer.accessHash),
      offsetId,
      limit: STORY_LIST_LIMIT,
    }),
    peerId: peer.id,
  });
}

export async function fetchPeerStoriesByIds({ peer, ids }: { peer: ApiPeer; ids: number[] }) {
  const request = new GramJs.stories.GetStoriesByID({
    peer: buildInputPeer(peer.id, peer.accessHash),
    id: ids,
  });
  let result;

  try {
    result = await invokeRequest(request, { shouldThrow: true });
  } catch (err) {
    if (err instanceof RPCError && err.errorMessage === 'CHANNEL_PRIVATE') {
      return {
        stories: buildDeletedStories(peer.id, ids),
      };
    }

    dispatchErrorUpdate(err as Error, request);
    return undefined;
  }

  if (!result) {
    return undefined;
  }

  const stories = ids.reduce<Record<number, ApiTypeStory>>((acc, id) => {
    const story = result.stories.find(({ id: currentId }) => currentId === id);
    if (story) {
      acc[id] = buildApiStory(peer.id, story);
    } else {
      acc[id] = {
        id,
        peerId: peer.id,
        isDeleted: true,
      };
    }

    return acc;
  }, {});

  // Add after building stories to avoid overwriting repair info
  result.stories.forEach((story) => addStoryToLocalDb(story, peer.id));

  return {
    pinnedIds: result.pinnedToTop,
    stories,
  };
}

function buildDeletedStories(peerId: string, ids: number[]): Record<number, ApiTypeStory> {
  return ids.reduce<Record<number, ApiTypeStory>>((acc, id) => {
    acc[id] = {
      id,
      peerId,
      isDeleted: true,
    };

    return acc;
  }, {});
}

export function viewStory({ peer, storyId }: { peer: ApiPeer; storyId: number }) {
  return invokeRequest(new GramJs.stories.IncrementStoryViews({
    peer: buildInputPeer(peer.id, peer.accessHash),
    id: [storyId],
  }));
}

export function markStoryRead({ peer, storyId }: { peer: ApiPeer; storyId: number }) {
  return invokeRequest(new GramJs.stories.ReadStories({
    peer: buildInputPeer(peer.id, peer.accessHash),
    maxId: storyId,
  }));
}

export function deleteStory({ peer, storyId }: { peer: ApiPeer; storyId: number }) {
  return invokeRequest(new GramJs.stories.DeleteStories({
    peer: buildInputPeer(peer.id, peer.accessHash),
    id: [storyId],
  }));
}

export function toggleStoryInProfile({
  peer, storyId, isInProfile,
}: {
  peer: ApiPeer; storyId: number; isInProfile?: boolean;
}) {
  return invokeRequest(new GramJs.stories.TogglePinned({
    peer: buildInputPeer(peer.id, peer.accessHash),
    id: [storyId],
    pinned: Boolean(isInProfile),
  }));
}

export function toggleStoryPinnedToTop({
  peer, storyIds,
}: {
  peer: ApiPeer; storyIds: number[];
}) {
  return invokeRequest(new GramJs.stories.TogglePinnedToTop({
    peer: buildInputPeer(peer.id, peer.accessHash),
    id: storyIds,
  }));
}

export async function fetchStoryViewList({
  peer,
  storyId,
  areJustContacts,
  query,
  areReactionsFirst,
  limit = STORY_LIST_LIMIT,
  offset = DEFAULT_PRIMITIVES.STRING,
}: {
  peer: ApiPeer;
  storyId: number;
  areJustContacts?: true;
  areReactionsFirst?: true;
  query?: string;
  limit?: number;
  offset?: string;
}) {
  const result = await invokeRequest(new GramJs.stories.GetStoryViewsList({
    peer: buildInputPeer(peer.id, peer.accessHash),
    id: storyId,
    justContacts: areJustContacts,
    q: query,
    reactionsFirst: areReactionsFirst,
    limit,
    offset,
  }));

  if (!result) {
    return undefined;
  }

  const views = result.views.map(buildApiStoryView).filter(Boolean);

  return {
    views,
    nextOffset: result.nextOffset,
    reactionsCount: result.reactionsCount,
    viewsCount: result.count,
  };
}

export async function fetchStoriesViews({
  peer,
  storyIds,
}: {
  peer: ApiPeer;
  storyIds: number[];
}) {
  const result = await invokeRequest(new GramJs.stories.GetStoriesViews({
    peer: buildInputPeer(peer.id, peer.accessHash),
    id: storyIds,
  }));

  if (!result?.views[0]) {
    return undefined;
  }

  const views = buildApiStoryViews(result.views[0]);

  return {
    views,
  };
}

export async function fetchStoryLink({ peer, storyId }: { peer: ApiPeer; storyId: number }) {
  const result = await invokeRequest(new GramJs.stories.ExportStoryLink({
    peer: buildInputPeer(peer.id, peer.accessHash),
    id: storyId,
  }));

  if (!result) {
    return undefined;
  }

  return result.link;
}

export async function reportStory({
  peer,
  storyId,
  description,
  option,
}: {
  peer: ApiPeer; storyId: number; description: string; option: string;
}) {
  try {
    const result = await invokeRequest(new GramJs.stories.Report({
      peer: buildInputPeer(peer.id, peer.accessHash),
      id: [storyId],
      option: deserializeBytes(option),
      message: description,
    }), { shouldThrow: true });

    if (!result) return undefined;

    return { result: buildApiReportResult(result), error: undefined };
  } catch (err: any) {
    const errorMessage = (err as ApiError).message;

    if (errorMessage === MESSAGE_ID_REQUIRED_ERROR) {
      return {
        result: undefined,
        error: errorMessage,
      };
    }

    throw err;
  }
}

export function editStoryPrivacy({
  peer,
  id,
  privacy,
}: {
  peer: ApiPeer;
  id: number;
  privacy: ApiInputPrivacyRules;
}) {
  return invokeRequest(new GramJs.stories.EditStory({
    peer: buildInputPeer(peer.id, peer.accessHash),
    id,
    privacyRules: buildInputPrivacyRules(privacy),
  }), {
    shouldReturnTrue: true,
  });
}

export function toggleStoriesHidden({
  peer,
  isHidden,
}: {
  peer: ApiPeer;
  isHidden: boolean;
}) {
  return invokeRequest(new GramJs.stories.TogglePeerStoriesHidden({
    peer: buildInputPeer(peer.id, peer.accessHash),
    hidden: isHidden,
  }));
}

export function fetchStoriesMaxIds({
  peers,
}: {
  peers: ApiPeer[];
}) {
  return invokeRequest(new GramJs.stories.GetPeerMaxIDs({
    id: peers.map((peer) => buildInputPeer(peer.id, peer.accessHash)),
  }), { shouldIgnoreErrors: true });
}

async function fetchCommonStoriesRequest({ method, peerId }: {
  method: GramJs.stories.GetPinnedStories | GramJs.stories.GetStoriesArchive;
  peerId: string;
}) {
  const result = await invokeRequest(method);

  if (!result) {
    return undefined;
  }

  const stories = buildCollectionByCallback(result.stories, (story) => (
    [story.id, buildApiStory(peerId, story)]
  ));

  // Add after building stories to avoid overwriting repair info
  result.stories.forEach((story) => addStoryToLocalDb(story, peerId));

  return {
    count: result.count,
    stories,
    pinnedIds: result.pinnedToTop,
  };
}

export function sendStoryReaction({
  peer, storyId, reaction, shouldAddToRecent,
}: {
  peer: ApiPeer;
  storyId: number;
  reaction?: ApiReaction;
  shouldAddToRecent?: boolean;
}) {
  return invokeRequest(new GramJs.stories.SendReaction({
    reaction: reaction ? buildInputReaction(reaction) : new GramJs.ReactionEmpty(),
    peer: buildInputPeer(peer.id, peer.accessHash),
    storyId,
    ...(shouldAddToRecent && { addToRecent: true }),
  }), {
    shouldReturnTrue: true,
  });
}

export function activateStealthMode({
  isForPast,
  isForFuture,
}: {
  isForPast?: true;
  isForFuture?: true;
}) {
  return invokeRequest(new GramJs.stories.ActivateStealthMode({
    past: isForPast,
    future: isForFuture,
  }), {
    shouldReturnTrue: true,
  });
}

export async function fetchAlbums({
  peer,
}: {
  peer: ApiPeer;
}): Promise<ApiStoryAlbum[] | undefined> {
  const result = await invokeRequest(new GramJs.stories.GetAlbums({
    peer: buildInputPeer(peer.id, peer.accessHash),
    hash: DEFAULT_PRIMITIVES.BIGINT,
  }));

  if (!result || result instanceof GramJs.stories.AlbumsNotModified) {
    return undefined;
  }

  return result.albums.map(buildApiStoryAlbum);
}

export async function fetchAlbumStories({
  peer,
  albumId,
  offset = 0,
  limit = STORY_LIST_LIMIT,
}: {
  peer: ApiPeer;
  albumId: number;
  offset?: number;
  limit?: number;
}): Promise<{
  stories: Record<number, ApiTypeStory>;
  pinnedIds?: number[];
  count: number;
} | undefined> {
  const result = await invokeRequest(new GramJs.stories.GetAlbumStories({
    peer: buildInputPeer(peer.id, peer.accessHash),
    albumId,
    offset,
    limit,
  }));

  if (!result) {
    return undefined;
  }

  const stories = buildCollectionByCallback(result.stories, (story) => (
    [story.id, buildApiStory(peer.id, story)]
  ));

  result.stories.forEach((story) => {
    if (story && story instanceof GramJs.StoryItem) {
      addStoryToLocalDb(story, peer.id);
    }
  });

  return {
    stories,
    pinnedIds: result.pinnedToTop,
    count: result.count,
  };
}
