import {useCallback, useMemo, useRef} from 'react';
import {createFilter} from 'react-select';
import {Theme} from '@emotion/react';
import styled from '@emotion/styled';
import debounce from 'lodash/debounce';

import {openCreateTeamModal} from 'sentry/actionCreators/modal';
import {addTeamToProject} from 'sentry/actionCreators/projects';
import {Button} from 'sentry/components/button';
import SelectControl, {
  ControlProps,
  GeneralSelectValue,
  StylesConfig,
} from 'sentry/components/forms/controls/selectControl';
import IdBadge from 'sentry/components/idBadge';
import {Tooltip} from 'sentry/components/tooltip';
import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants';
import {IconAdd, IconUser} from 'sentry/icons';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import {Organization, Project, Team} from 'sentry/types';
import useApi from 'sentry/utils/useApi';
import {useTeams} from 'sentry/utils/useTeams';
import withOrganization from 'sentry/utils/withOrganization';

const UnassignedWrapper = styled('div')`
  display: flex;
  align-items: center;
`;

const StyledIconUser = styled(IconUser)`
  margin-left: ${space(0.25)};
  margin-right: ${space(1)};
  color: ${p => p.theme.gray400};
`;

// An option to be unassigned on the team dropdown
const unassignedOption = {
  value: null,
  label: (
    <UnassignedWrapper>
      <StyledIconUser size="md" />
      {t('Unassigned')}
    </UnassignedWrapper>
  ),
  searchKey: 'unassigned',
  actor: null,
  disabled: false,
};

const CREATE_TEAM_VALUE = 'CREATE_TEAM_VALUE';

const optionFilter = createFilter({
  stringify: option => `${option.label} ${option.value}`,
});

const filterOption = (canditate, input) =>
  // Never filter out the create team option
  canditate.data.value === CREATE_TEAM_VALUE || optionFilter(canditate, input);

const getOptionValue = (option: TeamOption) => option.value;

// Ensures that the svg icon is white when selected
const unassignedSelectStyles: StylesConfig = {
  option: (provided, state) => {
    // XXX: The `state.theme` is an emotion theme object, but it is not typed
    // as the emotion theme object in react-select
    const theme = state.theme as unknown as Theme;

    return {...provided, svg: {color: state.isSelected ? theme.white : undefined}};
  },
};

const placeholderSelectStyles: StylesConfig = {
  input: (provided, state) => {
    // XXX: The `state.theme` is an emotion theme object, but it is not typed
    // as the emotion theme object in react-select
    const theme = state.theme as unknown as Theme;

    return {
      ...provided,
      display: 'grid',
      gridTemplateColumns: 'max-content 1fr',
      alignItems: 'center',
      gridGap: space(1),
      ':before': {
        backgroundColor: theme.backgroundSecondary,
        height: 24,
        width: 24,
        borderRadius: 3,
        content: '""',
        display: 'block',
      },
    };
  },
  placeholder: provided => ({
    ...provided,
    paddingLeft: 32,
  }),
};

type Props = {
  onChange: (value: any) => any;
  /**
   * Received via withOrganization
   * Note: withOrganization collects it from the context, this is not type safe
   */
  organization: Organization;
  /**
   * Controls whether the dropdown allows to create a new team
   */
  allowCreate?: boolean;
  includeUnassigned?: boolean;
  /**
   * Can be used to restrict teams to a certain project and allow for new teams to be add to that project
   */
  project?: Project;
  /**
   * Function to control whether a team should be shown in the dropdown
   */
  teamFilter?: (team: Team) => boolean;
  /**
   * Controls whether the value in the dropdown is a team id or team slug
   */
  useId?: boolean;
} & ControlProps;

type TeamActor = {
  id: string;
  name: string;
  type: 'team';
};

type TeamOption = GeneralSelectValue & {
  actor: TeamActor | null;
  searchKey: string;
};

function TeamSelector(props: Props) {
  const {
    allowCreate,
    includeUnassigned,
    styles: stylesProp,
    onChange,
    ...extraProps
  } = props;
  const {teamFilter, organization, project, multiple, value, useId} = props;

  const api = useApi();
  const {teams, fetching, onSearch} = useTeams();

  // TODO(ts) This type could be improved when react-select types are better.
  const selectRef = useRef<any>(null);

  const canCreateTeam = organization?.access?.includes('project:admin') ?? false;
  const canAddTeam = organization?.access?.includes('project:write') ?? false;

  const createTeamOption = useCallback(
    (team: Team): TeamOption => ({
      value: useId ? team.id : team.slug,
      label: `#${team.slug}`,
      leadingItems: <IdBadge team={team} hideName />,
      searchKey: team.slug,
      actor: {
        type: 'team',
        id: team.id,
        name: team.slug,
      },
    }),
    [useId]
  );

  /**
   * Closes the select menu by blurring input if possible since that seems to
   * be the only way to close it.
   */
  function closeSelectMenu() {
    if (!selectRef.current) {
      return;
    }

    const select = selectRef.current.select;
    const input: HTMLInputElement = select.inputRef;

    if (input) {
      // I don't think there's another way to close `react-select`
      input.blur();
    }
  }

  const handleAddTeamToProject = useCallback(
    async (team: Team) => {
      if (!project) {
        closeSelectMenu();
        return;
      }

      // Copy old value
      const oldValue = multiple ? [...(value ?? [])] : {value};
      // Optimistic update
      onChange?.(createTeamOption(team));

      try {
        await addTeamToProject(api, organization.slug, project.slug, team);
      } catch (err) {
        // Unable to add team to project, revert select menu value
        onChange?.(oldValue);
      }

      closeSelectMenu();
    },
    [api, createTeamOption, multiple, onChange, organization, project, value]
  );

  const createTeam = useCallback(
    () =>
      new Promise<TeamOption>(resolve => {
        openCreateTeamModal({
          organization,
          onClose: async team => {
            if (project) {
              await handleAddTeamToProject(team);
            }
            resolve(createTeamOption(team));
          },
        });
      }),
    [createTeamOption, handleAddTeamToProject, organization, project]
  );

  const handleChange = useCallback(
    (newValue: TeamOption | TeamOption[]) => {
      if (multiple) {
        const options = newValue as TeamOption[];
        const shouldCreate = options.find(option => option.value === CREATE_TEAM_VALUE);
        if (shouldCreate) {
          createTeam().then(newTeamOption => {
            onChange?.([
              ...options.filter(option => option.value !== CREATE_TEAM_VALUE),
              newTeamOption,
            ]);
          });
        } else {
          onChange?.(options);
        }
        return;
      }

      const option = newValue as TeamOption;
      if (option.value === CREATE_TEAM_VALUE) {
        createTeam().then(newTramOption => {
          onChange?.(newTramOption);
        });
      } else {
        onChange?.(option);
      }
    },
    [createTeam, multiple, onChange]
  );

  const createTeamOutsideProjectOption = useCallback(
    (team: Team): TeamOption => {
      // If the option/team is currently selected, optimistically assume it is now a part of the project
      if (value === (useId ? team.id : team.slug)) {
        return createTeamOption(team);
      }

      return {
        ...createTeamOption(team),
        disabled: true,
        label: `#${team.slug}`,
        leadingItems: <IdBadge team={team} hideName />,
        trailingItems: (
          <Tooltip
            title={
              canAddTeam
                ? t('Add %s to project', `#${team.slug}`)
                : t('You do not have permission to add team to project.')
            }
            containerDisplayMode="flex"
          >
            <AddToProjectButton
              size="zero"
              borderless
              disabled={!canAddTeam}
              onClick={() => handleAddTeamToProject(team)}
              icon={<IconAdd isCircled />}
              aria-label={t('Add %s to project', `#${team.slug}`)}
            />
          </Tooltip>
        ),
        tooltip: t('%s is not a member of project', `#${team.slug}`),
      };
    },
    [canAddTeam, createTeamOption, handleAddTeamToProject, useId, value]
  );

  function getOptions() {
    const filteredTeams = teamFilter ? teams.filter(teamFilter) : teams;
    const createOption = {
      value: CREATE_TEAM_VALUE,
      label: t('Create team'),
      leadingItems: <IconAdd isCircled />,
      searchKey: 'create',
      actor: null,
      disabled: !canCreateTeam,
      'data-test-id': 'create-team-option',
    };

    if (project) {
      const teamsInProjectIdSet = new Set(project.teams.map(team => team.id));
      const teamsInProject = filteredTeams.filter(team =>
        teamsInProjectIdSet.has(team.id)
      );
      const teamsNotInProject = filteredTeams.filter(
        team => !teamsInProjectIdSet.has(team.id)
      );

      return [
        ...(allowCreate ? [createOption] : []),
        ...teamsInProject.map(createTeamOption),
        ...teamsNotInProject.map(createTeamOutsideProjectOption),
        ...(includeUnassigned ? [unassignedOption] : []),
      ];
    }

    return [
      ...(allowCreate ? [createOption] : []),
      ...filteredTeams.map(createTeamOption),
      ...(includeUnassigned ? [unassignedOption] : []),
    ];
  }

  const options = useMemo(getOptions, [
    teamFilter,
    teams,
    canCreateTeam,
    project,
    allowCreate,
    createTeamOption,
    includeUnassigned,
    createTeamOutsideProjectOption,
  ]);

  const handleInputCHange = useMemo(
    () => debounce(val => void onSearch(val), DEFAULT_DEBOUNCE_DURATION),
    [onSearch]
  );

  const styles = useMemo(
    () => ({
      ...(includeUnassigned ? unassignedSelectStyles : {}),
      ...(multiple ? {} : placeholderSelectStyles),
      ...(stylesProp ?? {}),
    }),
    [includeUnassigned, multiple, stylesProp]
  );

  return (
    <SelectControl
      ref={selectRef}
      options={options}
      onInputChange={handleInputCHange}
      getOptionValue={getOptionValue}
      filterOption={filterOption}
      styles={styles}
      isLoading={fetching}
      onChange={handleChange}
      {...extraProps}
    />
  );
}

const AddToProjectButton = styled(Button)`
  flex-shrink: 0;
`;

export {TeamSelector};

// TODO(davidenwang): this is broken due to incorrect types on react-select
export default withOrganization(TeamSelector) as unknown as (
  p: Omit<Props, 'organization'>
) => JSX.Element;
