import type React from 'react';
import { useCallback, useId, useMemo, useState } from 'react';

import { FormattedMessage, useIntl } from 'react-intl';

import { useHistory } from 'react-router-dom';

import type { Map as ImmutableMap } from 'immutable';

import type { ApiMutedAccountJSON } from 'mastodon/api_types/accounts';
import type { ApiCollectionJSON } from 'mastodon/api_types/collections';
import { AccountListItem } from 'mastodon/components/account_list_item';
import { Avatar } from 'mastodon/components/avatar';
import { PendingBadge } from 'mastodon/components/badge';
import { Button } from 'mastodon/components/button';
import { DisplayName } from 'mastodon/components/display_name';
import { useAccountHandle } from 'mastodon/components/display_name/default';
import { EmptyState } from 'mastodon/components/empty_state';
import { FormStack, ComboboxField } from 'mastodon/components/form_fields';
import { useComboboxItemProps } from 'mastodon/components/form_fields/combobox_field';
import {
  ListItemContent,
  ListItemWrapper,
} from 'mastodon/components/list_item';
import {
  Article,
  ItemList,
} from 'mastodon/components/scrollable_list/components';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useSearchAccounts } from 'mastodon/hooks/useSearchAccounts';
import { domain } from 'mastodon/initial_state';
import type { Relationship } from 'mastodon/models/relationship';
import {
  addCollectionItem,
  getEditorCollectionItems,
  removeCollectionItem,
  updateCollectionEditorField,
} from 'mastodon/reducers/slices/collections';
import { useAppDispatch, useAppSelector } from 'mastodon/store';

import { PendingNote } from '../detail';
import { canAccountBeAdded, canAccountBeAddedByFollowers } from '../utils';

import classes from './styles.module.scss';
import { WizardStepTitle } from './wizard_step_title';

export const MAX_COLLECTION_ACCOUNT_COUNT = 25;

const AddedAccountItem: React.FC<{
  accountId: string;
  pending?: boolean;
  onRemove: (id: string) => void;
}> = ({ accountId, pending, onRemove }) => {
  const handleRemoveAccount = useCallback(() => {
    onRemove(accountId);
  }, [accountId, onRemove]);

  const renderButton = useCallback(
    () => (
      <Button compact secondary onClick={handleRemoveAccount}>
        <FormattedMessage
          id='collections.remove_account'
          defaultMessage='Remove'
        />
      </Button>
    ),
    [handleRemoveAccount],
  );

  return (
    <AccountListItem
      accountId={accountId}
      badge={pending && <PendingBadge />}
      renderButton={renderButton}
    />
  );
};

const SuggestedAccountItem: React.FC<{ id: string }> = ({ id }) => {
  const account = useAccount(id);
  const handle = useAccountHandle(account, domain);
  const comboboxItemProps = useComboboxItemProps();

  if (!account) return null;

  return (
    <li {...comboboxItemProps} className={classes.suggestion}>
      <ListItemWrapper icon={<Avatar account={account} size={40} />}>
        <ListItemContent subtitle={handle}>
          <DisplayName account={account} variant='simple' />
        </ListItemContent>
      </ListItemWrapper>
    </li>
  );
};

const renderAccountItem = (account: ApiMutedAccountJSON) => (
  <SuggestedAccountItem id={account.id} />
);

type GroupKey = 'available' | 'mustFollow' | 'disabled';

function groupSuggestions(
  accounts: ApiMutedAccountJSON[],
  relationships: ImmutableMap<string, Relationship>,
) {
  const { available, mustFollow, disabled } = Object.groupBy(
    accounts,
    (account): GroupKey => {
      if (canAccountBeAdded(account)) {
        return 'available';
      }

      if (
        canAccountBeAddedByFollowers(account) &&
        !relationships.get(account.id)?.following
      ) {
        return 'mustFollow';
      }

      return 'disabled';
    },
  );

  // Returning a new object ensures a fixed property order
  return { available, mustFollow, disabled };
}

const renderGroupTitle = (groupKey: GroupKey, titleId: string) => {
  if (groupKey === 'available') {
    return null;
  }

  let title: React.ReactElement;
  let description: React.ReactElement;

  if (groupKey === 'mustFollow') {
    title = (
      <FormattedMessage
        id='collections.suggestions.must_follow'
        defaultMessage='Must follow first'
      />
    );
    description = (
      <FormattedMessage
        id='collections.suggestions.must_follow_desc'
        defaultMessage='These accounts review all follow requests. Followers can add them to collections.'
      />
    );
  } else {
    title = (
      <FormattedMessage
        id='collections.suggestions.can_not_add'
        defaultMessage='Can’t be added'
      />
    );
    description = (
      <FormattedMessage
        id='collections.suggestions.can_not_add_desc'
        defaultMessage='These accounts may have opted out of discovery, or they might be on a server that doesn’t support collections.'
      />
    );
  }

  return (
    <li role='presentation'>
      <ListItemWrapper className={classes.suggestionGroup}>
        <ListItemContent id={titleId} subtitle={description}>
          {title}
        </ListItemContent>
      </ListItemWrapper>
    </li>
  );
};

const getItemId = (account: ApiMutedAccountJSON) => account.id;
const getIsItemDisabled = (account: ApiMutedAccountJSON) =>
  !canAccountBeAdded(account);

export const CollectionAccounts: React.FC<{
  collection?: ApiCollectionJSON | null;
}> = ({ collection }) => {
  const intl = useIntl();
  const dispatch = useAppDispatch();
  const history = useHistory();

  const { id, items: collectionItems } = collection ?? {};
  const isEditMode = !!id;

  const editorItemsFromState = useAppSelector(
    (state) => state.collections.editor.items,
  );

  // In edit mode, we're bypassing our Redux state and just work on the
  // collection items directly since they're edited "live", saving right
  // after each addition/deletion
  const editorItems = useMemo(
    () =>
      isEditMode
        ? getEditorCollectionItems(collectionItems)
        : editorItemsFromState,
    [isEditMode, collectionItems, editorItemsFromState],
  );
  const hasPendingItems = editorItems.some((item) => item.state === 'pending');

  const [searchValue, setSearchValue] = useState('');

  const hasItems = editorItems.length > 0;
  const hasMaxItems = editorItems.length === MAX_COLLECTION_ACCOUNT_COUNT;

  const wasAccountAdded = useCallback(
    (account: ApiMutedAccountJSON) =>
      !!editorItems.find((item) => item.account_id === account.id),
    [editorItems],
  );

  const {
    accounts: suggestedAccounts,
    isLoading: isLoadingSuggestions,
    searchAccounts,
    resetAccounts,
  } = useSearchAccounts({
    withRelationships: true,
    withDefaultFollows: searchValue === '',
    // Don't suggest accounts that were already added
    filterResults: (account) => !wasAccountAdded(account),
  });

  const relationships = useAppSelector((state) => state.relationships);

  const groupedItems = groupSuggestions(suggestedAccounts, relationships);

  const handleSearchValueChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      setSearchValue(e.target.value);
      searchAccounts(e.target.value);
    },
    [searchAccounts],
  );

  const handleSearchKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLInputElement>) => {
      if (e.key === 'Enter') {
        e.preventDefault();
      }
    },
    [],
  );

  const removeAccountItem = useCallback(
    (accountId: string) => {
      dispatch(
        updateCollectionEditorField({
          field: 'items',
          value: editorItems.filter((item) => item.account_id !== accountId),
        }),
      );
    },
    [editorItems, dispatch],
  );

  const addAccountItem = useCallback(
    (item: ApiMutedAccountJSON) => {
      if (!wasAccountAdded(item)) {
        dispatch(
          updateCollectionEditorField({
            field: 'items',
            value: [
              ...editorItems,
              {
                account_id: item.id,
                state:
                  item.feature_approval.current_user === 'manual'
                    ? 'pending'
                    : 'accepted',
              },
            ],
          }),
        );
      }
    },
    [editorItems, wasAccountAdded, dispatch],
  );

  const instantRemoveAccountItem = useCallback(
    (accountId: string) => {
      const itemId = collectionItems?.find(
        (item) => item.account_id === accountId,
      )?.id;
      if (itemId && id) {
        if (
          window.confirm(
            intl.formatMessage({
              id: 'collections.confirm_account_removal',
              defaultMessage:
                'Are you sure you want to remove this account from this collection?',
            }),
          )
        ) {
          void dispatch(removeCollectionItem({ collectionId: id, itemId }));
        }
      }
    },
    [collectionItems, dispatch, id, intl],
  );

  const instantAddAccountItem = useCallback(
    (item: ApiMutedAccountJSON) => {
      if (id && !wasAccountAdded(item)) {
        void dispatch(
          addCollectionItem({ collectionId: id, accountId: item.id }),
        );
      }
    },
    [dispatch, id, wasAccountAdded],
  );

  const handleRemoveAccountItem = useCallback(
    (accountId: string) => {
      if (isEditMode) {
        instantRemoveAccountItem(accountId);
      } else {
        removeAccountItem(accountId);
      }
    },
    [isEditMode, instantRemoveAccountItem, removeAccountItem],
  );

  const handleSelectItem = useCallback(
    (item: ApiMutedAccountJSON) => {
      if (isEditMode) {
        instantAddAccountItem(item);
      } else {
        addAccountItem(item);
      }

      setSearchValue('');
      resetAccounts();
    },
    [addAccountItem, instantAddAccountItem, isEditMode, resetAccounts],
  );

  const handleSubmit: React.SubmitEventHandler = useCallback(
    (e) => {
      e.preventDefault();

      if (!id) {
        history.push('/collections/new/details');
      }
    },
    [id, history],
  );

  const inputId = useId();
  const AccountsHeadingElement = id ? 'h2' : 'h3';

  return (
    <form onSubmit={handleSubmit} className={classes.form}>
      <FormStack className={classes.formFieldStack}>
        <header className={classes.header}>
          {!id && (
            <WizardStepTitle
              step={1}
              title={
                <FormattedMessage
                  id='collections.create.accounts_title'
                  defaultMessage='Who will you feature in this collection?'
                />
              }
            />
          )}
          {hasPendingItems && <PendingNote />}
          <ComboboxField
            openOnFocus
            id={inputId}
            label={intl.formatMessage({
              id: 'collections.search_accounts_label',
              defaultMessage: 'Search for an account to add',
            })}
            value={hasMaxItems ? '' : searchValue}
            onChange={handleSearchValueChange}
            onKeyDown={handleSearchKeyDown}
            disabled={hasMaxItems}
            isLoading={isLoadingSuggestions}
            items={groupedItems}
            getItemId={getItemId}
            getIsItemDisabled={getIsItemDisabled}
            renderItem={renderAccountItem}
            renderGroupTitle={renderGroupTitle}
            onSelectItem={handleSelectItem}
            status={
              hasMaxItems
                ? {
                    variant: 'warning',
                    message: intl.formatMessage({
                      id: 'collections.search_accounts_max_reached',
                      defaultMessage:
                        'You have added the maximum number of accounts',
                    }),
                  }
                : null
            }
          />
        </header>

        <div>
          {hasItems && (
            <AccountsHeadingElement className={classes.listHeading}>
              <FormattedMessage
                id='collections.hints.accounts_counter'
                defaultMessage='{count}/{max} accounts'
                values={{
                  count: editorItems.length,
                  max: MAX_COLLECTION_ACCOUNT_COUNT,
                }}
              />
            </AccountsHeadingElement>
          )}

          <ItemList
            className={classes.accountList}
            emptyMessage={
              <EmptyState
                title={
                  <FormattedMessage
                    id='collections.accounts.empty_editor_title'
                    defaultMessage='No one is in this collection yet'
                  />
                }
                message={
                  <FormattedMessage
                    id='collections.accounts.empty_description'
                    defaultMessage='Add up to {count} accounts'
                    values={{
                      count: MAX_COLLECTION_ACCOUNT_COUNT,
                    }}
                  />
                }
              />
            }
          >
            {editorItems.map(({ account_id, state }, index) => (
              <Article
                key={account_id}
                aria-posinset={index}
                aria-setsize={editorItems.length}
              >
                <AddedAccountItem
                  accountId={account_id}
                  pending={state === 'pending'}
                  onRemove={handleRemoveAccountItem}
                />
              </Article>
            ))}
          </ItemList>
        </div>
      </FormStack>
      {!isEditMode && hasItems && (
        <div className={classes.stickyFooter}>
          <Button type='submit'>
            {id ? (
              <FormattedMessage id='lists.save' defaultMessage='Save' />
            ) : (
              <FormattedMessage
                id='collections.continue'
                defaultMessage='Continue'
              />
            )}
          </Button>
        </div>
      )}
    </form>
  );
};
