import { isAutocomplete, isMessageComponent, isModalSubmit, isPing, type CommandContext } from '@commands';
import {
  Constants,
  type InteractionContentEdit,
  type InteractionModalContent,
  type CommandInteraction,
  type ComponentInteraction,
  type ModalSubmitInteraction,
  Interaction,
} from '@projectdysnomia/dysnomia';

export type PagesInteraction = CommandInteraction | ModalSubmitInteraction | ComponentInteraction;
export enum PageType {
  MODAL = 'modal',
  MESSAGE = 'message',
  FOLLOWUP = 'followup',
}

export interface Page<T> {
  key: string;
  type?: PageType; // defaults to MESSAGE
  followUpFlags?: number;
  render: (
    ctx: PageContext<T>,
  ) => (InteractionModalContent | InteractionContentEdit) | Promise<InteractionModalContent | InteractionContentEdit>;
}

export interface PagesOptions<T> {
  pages: Record<string, Page<T>>;
  initialPage?: string;
  timeout?: number; // in seconds
  ephemeral?: boolean; // whether the initial message should be ephemeral
  useEmbeds?: boolean; // will not enable components v2
  initialStateData?: T; // initial state to merge with default state
  router?: (ctx: PageContext<T>) => string; // function to determine the next page key
}

export interface PageState<T> {
  currentPage: string;
  timeoutAt: number; // timestamp in ms
  lastInteractionAt?: number; // timestamp in ms
  messageId?: string;
  channelId?: string;
  data: T;
}

export interface PageContext<T> {
  state: PageState<T>;
  custom_id: string; // current interaction custom_id
  interaction: PagesInteraction;
  goToPage: (pageKey: string) => Promise<InteractionContentEdit>;
}

function createPageContext<T>(interaction: PagesInteraction, options: PagesOptions<T>, state: PageState<T>): PageContext<T> {
  return {
    state,
    interaction,
    custom_id: 'custom_id' in interaction.data ? interaction.data.custom_id : (options.initialPage ?? 'root'),
    goToPage: (pageKey: string) => {
      const page = options.pages[pageKey];
      this.state.currentPage = pageKey;
      if (!page) {
        throw new Error(`Page with key "${pageKey}" not found`);
      }
      return page.render(createPageContext(interaction, options, { ...state, currentPage: pageKey })) as Promise<InteractionContentEdit>;
    },
  };
}

function defaultPageState<T>(options: PagesOptions<T>): PageState<T> {
  const timeoutAt = options.timeout ? Date.now() + options.timeout * 1000 : Infinity;
  return {
    currentPage: options.initialPage ?? options.pages[0].key,
    timeoutAt,
    lastInteractionAt: Date.now(),
    data: options.initialStateData ?? ({} as T),
  };
}

function getPageState<T>(options: PagesOptions<T>, cmdCtx: CommandContext & { state: { __pageState?: PageState<T> } }) {
  const cmdState = cmdCtx.state;
  if ('__pageState' in cmdState && cmdState.__pageState) {
    return cmdState.__pageState as PageState<T>;
  }
  cmdState.__pageState = defaultPageState(options);
  return cmdState.__pageState as PageState<T>;
}

function validateOptions<T>(options: PagesOptions<T>) {
  const keys = Object.keys(options.pages);
  const uniqueKeys = new Set(keys);
  if (uniqueKeys.size !== keys.length) {
    throw new Error('Duplicate page keys found');
  }
}

function getFlags(options: PagesOptions<any>) {
  let flags = 0;
  if (options.ephemeral) {
    flags |= Constants.MessageFlags.EPHEMERAL;
  }
  if (!options.useEmbeds) {
    flags |= Constants.MessageFlags.IS_COMPONENTS_V2;
  }
  return flags;
}

export async function usePages<T>(options: PagesOptions<T>, interaction: Interaction, cmdCtx: CommandContext) {
  if (isAutocomplete(interaction) || isPing(interaction)) {
    throw new Error('usePages cannot be used with autocomplete or ping interactions');
  }
  const pagesInteraction = interaction as PagesInteraction;
  validateOptions(options);
  const pageState = getPageState(options, cmdCtx);
  const pageContext = createPageContext(pagesInteraction, options, pageState);
  const pageKey =
    options.router ? options.router(pageContext) : (pageContext.custom_id ?? options.initialPage ?? Object.keys(options.pages)[0]);
  // if we have subroutes, we only want the main route from the page key
  const page = options.pages[pageKey.split(':')[0]] ?? options.pages[0];
  pageContext.state.currentPage = page.key;

  if (page.type === PageType.MODAL && !isModalSubmit(pagesInteraction)) {
    // we don't defer modals and can't respond to a modal with a modal.
    const cnt = page.render(pageContext);
    const content = isPromise(cnt) ? await cnt : cnt;
    return await pagesInteraction.createModal(content as InteractionModalContent);
  }

  if (page.type === PageType.FOLLOWUP) {
    if (!pageState.messageId) {
      throw new Error('Cannot send a followup message before an initial message has been sent');
    }
    const flags = page.type === PageType.FOLLOWUP ? (page.followUpFlags ?? getFlags(options)) : getFlags(options);
    await pagesInteraction.defer(flags);
    const cnt = page.render(pageContext);
    const content = isPromise(cnt) ? await cnt : cnt;
    return await pagesInteraction.createFollowup({
      flags,
      ...(content as InteractionContentEdit),
    });
  }

  if (pageState.messageId && isMessageComponent(pagesInteraction)) {
    await pagesInteraction.deferUpdate();
    const cnt = page.render(pageContext);
    const content = isPromise(cnt) ? await cnt : cnt;
    return await pagesInteraction.editMessage(pageState.messageId, content as InteractionContentEdit);
  }

  {
    await pagesInteraction.defer(getFlags(options));
    const cnt = page.render(pageContext);
    const content = isPromise(cnt) ? await cnt : cnt;
    const message = await pagesInteraction.createFollowup({
      flags: getFlags(options),
      ...(content as InteractionContentEdit),
    });
    pageState.messageId = message.id;
    pageState.channelId = message.channel?.id;
    return message;
  }
}

function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
  return typeof (value as Promise<T>)?.then === 'function';
}
