import {
  batch,
  createStore,
  mergeStore,
  omit,
  setup,
  sync,
  throwOnConflictingProps,
} from "@ariakit/store";
import type { Store, StoreOptions, StoreProps } from "@ariakit/store";
import { toArray, defaultValue } from "@ariakit/utils";
import type { PickRequired, SetState } from "@ariakit/utils";
import type { ComboboxStore } from "../combobox/combobox-store.ts";
import type {
  CompositeStoreFunctions,
  CompositeStoreItem,
  CompositeStoreOptions,
  CompositeStoreState,
} from "../composite/composite-store.ts";
import { createCompositeStore } from "../composite/composite-store.ts";
import type {
  PopoverStoreFunctions,
  PopoverStoreOptions,
  PopoverStoreState,
} from "../popover/popover-store.ts";
import { createPopoverStore } from "../popover/popover-store.ts";

type MutableValue<T extends SelectStoreValue = SelectStoreValue> =
  T extends string ? string : T;

/** @deprecated Use `createComboboxStore` instead. */
export function createSelectStore<
  T extends SelectStoreValue = SelectStoreValue,
>(
  props: PickRequired<SelectStoreProps<T>, "value" | "defaultValue">,
): SelectStore<T>;

/** @deprecated Use `createComboboxStore` instead. */
export function createSelectStore(props?: SelectStoreProps): SelectStore;

export function createSelectStore({
  combobox,
  ...props
}: SelectStoreProps = {}): SelectStore {
  const store = mergeStore(
    props.store,
    omit(combobox, [
      "value",
      "items",
      "renderedItems",
      "compositeElement",
      "baseElement",
      "arrowElement",
      "anchorElement",
      "contentElement",
      "popoverElement",
      "disclosureElement",
      "inputElement",
      "labelElement",
      "selectElement",
      "selectLabelElement",
    ]),
  );

  throwOnConflictingProps(props, store);

  const syncState = store.getState();

  const composite = createCompositeStore({
    ...props,
    store,
    virtualFocus: defaultValue(
      props.virtualFocus,
      syncState.virtualFocus,
      true,
    ),
    compositeElementInFocusOrder: defaultValue(
      props.compositeElementInFocusOrder,
      props.includesBaseElement,
      syncState.compositeElementInFocusOrder,
      syncState.includesBaseElement,
      false,
    ),
    activeId: defaultValue(
      props.activeId,
      syncState.activeId,
      props.defaultActiveId,
      null,
    ),
    orientation: defaultValue(
      props.orientation,
      syncState.orientation,
      "vertical" as const,
    ),
  });

  const popover = createPopoverStore({
    ...props,
    store,
    placement: defaultValue(
      props.placement,
      syncState.placement,
      "bottom-start" as const,
    ),
  });

  const initialValue = new String("") as "";

  const initialState: SelectStoreState = {
    ...composite.getState(),
    ...popover.getState(),
    value: defaultValue(
      props.value,
      syncState.value,
      props.defaultValue,
      initialValue,
    ),
    setValueOnMove: defaultValue(
      props.setValueOnMove,
      syncState.setValueOnMove,
      false,
    ),
    labelElement: defaultValue(syncState.labelElement, null),
    selectElement: defaultValue(syncState.selectElement, null),
    listElement: defaultValue(syncState.listElement, null),
  };

  const select = createStore(initialState, composite, popover, store);

  // Initialize an unset value from the first enabled item.
  setup(select, () =>
    sync(select, ["value", "items"], (state) => {
      if (state.value !== initialValue) return;
      if (!state.items.length) return;
      const item = state.items.find(
        (item) => !item.disabled && item.value != null,
      );
      if (item?.value == null) return;
      select.setState("value", item.value);
    }),
  );

  // Resets the active id to its initial state when the popover is hidden. This
  // guarantees that the active id won't be pointing to another item when the
  // popover is shown again, which would cause the selected item to not be
  // auto-focused. See test "clicking on different tab and clicking outside
  // resets the selected tab".
  setup(select, () =>
    sync(select, ["mounted"], (state) => {
      if (state.mounted) return;
      select.setState("activeId", initialState.activeId);
    }),
  );

  // Keep the selected item active while the popover is closed.
  setup(select, () =>
    sync(select, ["mounted", "items", "value"], (state) => {
      // TODO: Revisit with the "open with keyboard, then try to open
      // again" test when deprecating ComboboxProvider as a
      // SelectProvider parent.
      // A composed Combobox owns active-item synchronization.
      if (combobox) return;
      if (state.mounted) return;
      const values = toArray(state.value);
      const lastValue = values[values.length - 1];
      if (lastValue == null) return;
      const item = state.items.find(
        (item) => !item.disabled && item.value === lastValue,
      );
      if (!item) return;
      select.setState("activeId", item.id);
    }),
  );

  // Update the value after composite movement when configured.
  setup(select, () =>
    batch(select, ["setValueOnMove", "moves"], (state) => {
      const { mounted, value, activeId } = select.getState();
      if (!state.setValueOnMove && mounted) return;
      if (Array.isArray(value)) return;
      if (!state.moves) return;
      if (!activeId) return;
      const item = composite.item(activeId);
      if (!item || item.disabled || item.value == null) return;
      select.setState("value", item.value);
    }),
  );

  return {
    ...composite,
    ...popover,
    ...select,
    combobox,
    setValue: (value) => select.setState("value", value),
    setLabelElement: (element) => {
      select.setState("labelElement", element);
      combobox?.setSelectLabelElement(element);
    },
    setSelectElement: (element) => select.setState("selectElement", element),
    setListElement: (element) => select.setState("listElement", element),
  };
}

/** @deprecated Use `ComboboxStoreSelectedValue` instead. */
export type SelectStoreValue = string | readonly string[];

/** @deprecated Use `ComboboxStoreItem` instead. */
export interface SelectStoreItem extends CompositeStoreItem {
  value?: string;
}

/** @deprecated Use `ComboboxStoreState` instead. */
export interface SelectStoreState<T extends SelectStoreValue = SelectStoreValue>
  extends CompositeStoreState<SelectStoreItem>, PopoverStoreState {
  /** @default true */
  virtualFocus: CompositeStoreState<SelectStoreItem>["virtualFocus"];
  /** @default null */
  activeId: CompositeStoreState<SelectStoreItem>["activeId"];
  /** @default "vertical" */
  orientation: CompositeStoreState<SelectStoreItem>["orientation"];
  /** @default "bottom-start" */
  placement: PopoverStoreState["placement"];
  /**
   * The select value.
   */
  value: MutableValue<T>;
  /**
   * Whether the select
   * [`value`](https://ariakit.com/reference/select-provider#value) should be
   * set when the active item changes by moving (which usually happens when
   * moving to an item using the keyboard).
   * @default false
   */
  setValueOnMove: boolean;
  /**
   * The select label element.
   */
  labelElement: HTMLElement | null;
  /**
   * The select button element.
   */
  selectElement: HTMLElement | null;
  /**
   * The select list element.
   */
  listElement: HTMLElement | null;
}

/** @deprecated Use `ComboboxStoreFunctions` instead. */
export interface SelectStoreFunctions<
  T extends SelectStoreValue = SelectStoreValue,
>
  extends
    Pick<SelectStoreOptions<T>, "combobox">,
    CompositeStoreFunctions<SelectStoreItem>,
    PopoverStoreFunctions {
  /**
   * Sets the [`value`](https://ariakit.com/reference/select-provider#value)
   * state.
   * @example
   * store.setValue("Apple");
   * store.setValue(["Apple", "Banana"]);
   * store.setValue((value) => value === "Apple" ? "Banana" : "Apple"));
   */
  setValue: SetState<SelectStoreState<T>["value"]>;
  /**
   * Sets the `labelElement` state.
   */
  setLabelElement: SetState<SelectStoreState<T>["labelElement"]>;
  /**
   * Sets the `selectElement` state.
   */
  setSelectElement: SetState<SelectStoreState<T>["selectElement"]>;
  /**
   * Sets the `listElement` state.
   */
  setListElement: SetState<SelectStoreState<T>["listElement"]>;
}

/** @deprecated Use `ComboboxStoreOptions` instead. */
export interface SelectStoreOptions<
  T extends SelectStoreValue = SelectStoreValue,
>
  extends
    StoreOptions<
      SelectStoreState<T>,
      | "virtualFocus"
      | "activeId"
      | "orientation"
      | "placement"
      | "value"
      | "setValueOnMove"
    >,
    CompositeStoreOptions<SelectStoreItem>,
    PopoverStoreOptions {
  /**
   * A reference to a combobox store. This is used when combining the combobox
   * with a select (e.g., select with a search input). The stores will share the
   * same state.
   */
  combobox?: ComboboxStore | null;
  /**
   * The default value. If not set, the first non-disabled item will be used.
   */
  defaultValue?: SelectStoreState<T>["value"];
}

/** @deprecated Use `ComboboxStoreProps` instead. */
export interface SelectStoreProps<T extends SelectStoreValue = SelectStoreValue>
  extends SelectStoreOptions<T>, StoreProps<SelectStoreState<T>> {}

/** @deprecated Use `ComboboxStore` instead. */
export interface SelectStore<T extends SelectStoreValue = SelectStoreValue>
  extends SelectStoreFunctions<T>, Store<SelectStoreState<T>> {}
