import { Component, SyntheticEvent, ReactNode, Fragment, KeyboardEvent, FC } from 'react';

import {
    Checkbox,
    TableFilterCellProps,
    Radio,
    Input,
    Spinner,
    BodyText,
} from '@servicetitan/design-system';
import { IdType } from '@servicetitan/data-query';

import {
    CustomColumnMenuFilterSingleOpts,
    renderCustomColumnMenuFilter,
} from '../column-menu-filters';
import { makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';

export type AsyncSelectFilterDataFetcher<
    TV extends IdType = IdType,
    TO extends AsyncSelectItem<TV> = AsyncSelectItem<TV>,
> = (opts: { search?: string }) => Promise<{ data: TO[] }>;

export interface AsyncSelectItem<TV extends IdType = IdType> {
    value: TV;
    text: string;
}

interface SelectorProps<TID extends IdType, TO extends AsyncSelectItem<TID>> {
    placeholder?: string;
    selected: TO[];
    dataFetcher: AsyncSelectFilterDataFetcher<TID, TO>;
    itemComponent: FC<SelectorItemProps>;
    onChange(option: TO, checked: boolean, event: SyntheticEvent<HTMLInputElement>): void;
    renderer?(item: TO): ReactNode;
}

interface SelectorItemProps {
    option: AsyncSelectItem;
    checked: boolean;
    onChange(
        option: AsyncSelectItem,
        checked: boolean,
        event: SyntheticEvent<HTMLInputElement>
    ): void;
    renderer?(item: AsyncSelectItem): ReactNode;
}

@observer
class SelectorAsync<TID extends IdType, TO extends AsyncSelectItem<TID>> extends Component<
    SelectorProps<TID, TO>
> {
    @observable shownOptions: AsyncSelectItem<TID>[] = [];
    @observable search = '';
    @observable error = false;
    @observable loading = false;

    constructor(props: SelectorProps<TID, TO>) {
        super(props);
        makeObservable(this);
    }

    componentDidMount() {
        this.searchOptions().catch();
    }

    handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
        if (event.key === 'Enter') {
            event.stopPropagation();
        }
    };

    handleSearch = (_0: SyntheticEvent<HTMLInputElement>, data: { value: string }) => {
        runInAction(() => (this.search = data.value));
        this.searchOptions().catch();
    };

    searchOptions = async () => {
        runInAction(() => {
            this.loading = true;
        });

        try {
            const { data } = await this.props.dataFetcher({
                search: this.search,
            });

            runInAction(() => {
                this.shownOptions = data;
                this.error = false;
                this.loading = false;
            });
        } catch {
            runInAction(() => {
                this.error = true;
                this.loading = false;
            });
        }
    };

    render() {
        const selectedOptions = this.props.selected;
        const selected = new Set(selectedOptions.map(opt => opt.value));
        const ItemComponent = this.props.itemComponent;

        return (
            <Fragment>
                <Input
                    className="m-x-half m-t-half m-b-2"
                    placeholder={this.props.placeholder}
                    onChange={this.handleSearch}
                    onKeyDown={this.handleKeyDown}
                    size="xsmall"
                    value={this.search}
                />
                <div className="p-x-half position-relative" onKeyDown={this.handleKeyDown}>
                    {!!selectedOptions.length && (
                        <div className="border-bottom m-y-half">
                            {selectedOptions.map(option => (
                                <ItemComponent
                                    key={option.value}
                                    option={option}
                                    checked
                                    renderer={this.props.renderer}
                                    onChange={this.props.onChange}
                                />
                            ))}
                        </div>
                    )}
                    {this.error ? (
                        <BodyText className="c-red-500">Unable to load options</BodyText>
                    ) : this.shownOptions.length ? (
                        this.shownOptions
                            .filter(opt => !selected.has(opt.value))
                            .map(option => (
                                <ItemComponent
                                    key={option.value}
                                    option={option}
                                    checked={false}
                                    renderer={this.props.renderer}
                                    onChange={this.props.onChange}
                                />
                            ))
                    ) : this.loading ? (
                        <BodyText>&nbsp;</BodyText>
                    ) : (
                        <BodyText>No options found</BodyText>
                    )}

                    {this.loading && (
                        <div className="position-absolute top-0 bottom-0 left-0 right-0 opacity-disabled bg-white d-f justify-content-center">
                            <Spinner size="tiny" />
                        </div>
                    )}
                </div>
            </Fragment>
        );
    }
}

const SelectorItemSingle: FC<SelectorItemProps> = ({ option, renderer, checked, onChange }) => (
    <Radio
        label={renderer?.(option) ?? option.text}
        checked={checked}
        onChange={(_, event) => onChange(option, true, event)}
        className="m-b-1"
    />
);

const SelectorItemMultiple: FC<SelectorItemProps> = ({ option, renderer, checked, onChange }) => (
    <Checkbox
        label={renderer?.(option) ?? option.text}
        checked={checked}
        onChange={(_, checked, event) => onChange(option, checked, event)}
        className="m-b-1"
    />
);

export interface AsyncSelectFilterOptions<
    TID extends IdType,
    TO extends AsyncSelectItem<TID>,
> extends CustomColumnMenuFilterSingleOpts {
    dataFetcher: AsyncSelectFilterDataFetcher<TID, TO>;
    placeholder?: string;
    multiple?: boolean;
    renderItem?: (item: TO) => ReactNode;
}

interface TableFilterCellPropsTyped<T = any> extends Omit<TableFilterCellProps, 'value'> {
    value?: T;
}

/**
 * @deprecated use selectColumnMenuFilter instead
 */
export function asyncSelectColumnMenuFilter<TID extends IdType, TO extends AsyncSelectItem<TID>>({
    dataFetcher,
    placeholder,
    multiple,
    renderItem,
    ...opts
}: AsyncSelectFilterOptions<TID, TO>) {
    const contains = (value: TID, options?: TO[]) => options?.some(opt => opt.value === value);
    const equals = (value: TID, option?: TO) => option?.value === value;

    const FilterCell = multiple
        ? ({ value, onChange }: TableFilterCellPropsTyped<TO[]>) => {
              const handleChange = (
                  option: TO,
                  checked: boolean,
                  event: SyntheticEvent<HTMLInputElement>
              ) => {
                  const val = checked
                      ? (value ?? []).concat(option)
                      : (value ?? []).filter(opt => opt.value !== option.value);

                  onChange({
                      value: val.length ? val : undefined,
                      operator: val.length ? contains : '',
                      syntheticEvent: event,
                  });
              };

              return (
                  <SelectorAsync
                      selected={value ?? []}
                      itemComponent={SelectorItemMultiple}
                      onChange={handleChange}
                      placeholder={placeholder}
                      renderer={renderItem}
                      dataFetcher={dataFetcher}
                  />
              );
          }
        : ({ value, onChange }: TableFilterCellPropsTyped<TO>) => {
              const handleChange = (
                  option: TO,
                  checked: boolean,
                  event: SyntheticEvent<HTMLInputElement>
              ) => {
                  onChange({
                      value: option,
                      operator: option ? equals : '',
                      syntheticEvent: event,
                  });
              };

              return (
                  <SelectorAsync
                      selected={value ? [value] : []}
                      onChange={handleChange}
                      itemComponent={SelectorItemSingle}
                      placeholder={placeholder}
                      renderer={renderItem}
                      dataFetcher={dataFetcher}
                  />
              );
          };

    return renderCustomColumnMenuFilter(FilterCell, opts);
}
