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, toJS } from 'mobx';
import { observer } from 'mobx-react';
import { getSimpleValue } from './value-getter';
import { objectSearch } from './object-search';
import { selectColumnMenuFilterOperators } from './operators';

export type SelectFilterDataFetcher<TO> = (opts: { search?: string }) => Promise<{ data: TO[] }>;

export interface SelectFilterSearchOptions<TO = any> {
    placeholder?: string;
    filter(search: string): (item: TO) => boolean | undefined;
}

interface SelectorProps<TO> {
    search?: SelectFilterSearchOptions<TO>;
    selected: TO[];
    data?: TO[];
    dataFetcher?: SelectFilterDataFetcher<TO>;
    itemComponent: FC<SelectorItemProps<TO>>;
    onChange(option: TO, checked: boolean, event: SyntheticEvent<HTMLInputElement>): void;
    valueSelector(item: TO): IdType;
    renderItem(item: TO): ReactNode;
}

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

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

    constructor(props: SelectorProps<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 () => {
        if (this.props.dataFetcher) {
            runInAction(() => {
                this.loading = true;
            });
        }

        try {
            const data = await this.getData();

            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 => this.props.valueSelector(opt)));
        const ItemComponent = this.props.itemComponent;

        return (
            <Fragment>
                {!!this.props.search && (
                    <Input
                        className="m-x-half m-t-half m-b-2"
                        placeholder={this.props.search?.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, index) => (
                                <ItemComponent
                                    key={this.getItemKey(option, index)}
                                    option={option}
                                    checked
                                    renderer={this.props.renderItem}
                                    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(this.props.valueSelector(opt)))
                            .map((option, index) => (
                                <ItemComponent
                                    key={this.getItemKey(option, index)}
                                    option={option}
                                    checked={false}
                                    renderer={this.props.renderItem}
                                    onChange={this.props.onChange}
                                />
                            ))
                    ) : this.loading ? (
                        <BodyText>&nbsp;</BodyText>
                    ) : (
                        <BodyText subdued>No options match search criteria</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>
        );
    }

    private async getData(): Promise<TO[]> {
        if (this.props.dataFetcher) {
            const { data } = await this.props.dataFetcher({
                search: this.search,
            });

            return data;
        }

        let data = this.props.data ?? [];

        if (this.props.search?.filter) {
            data = data.filter(this.props.search.filter(this.search));
        }
        return data;
    }

    private getItemKey = (item: TO, index: number) => {
        return `${index}__${this.props.valueSelector(item)}`;
    };
}

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

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

export interface SelectFilterOptions<TO> extends CustomColumnMenuFilterSingleOpts {
    /** Can select multiple options in filter */
    multiple?: boolean;
    /** Ability to search options in filter */
    search?: boolean | Partial<SelectFilterSearchOptions>;
    /** Static options to show in filter */
    data?: TO[];
    /** Method to fetch filter options asynchronously */
    dataFetcher?: SelectFilterDataFetcher<TO>;
    /** Search operator passed to table state */
    operator?: ((value: any, options?: TO[]) => boolean) | ((value: any, options?: TO) => boolean);
    /** Select item value (ex id) for complex items */
    valueSelector?(item: TO): IdType;
    /** Select row item value (from table source row field) for complex items */
    rowValueSelector?(item: any): IdType | undefined;
    /** Render option label */
    renderItem?(item: TO): ReactNode;
}

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

export function selectColumnMenuFilter<TO>({
    dataFetcher,
    data,
    search,
    multiple,
    valueSelector = getSimpleValue,
    rowValueSelector = getSimpleValue,
    renderItem,
    operator,
    ...opts
}: SelectFilterOptions<TO>) {
    const renderer = renderItem ?? (item => valueSelector(item));

    const searchOptions = search
        ? {
              filter: objectSearch,
              ...(typeof search === 'boolean' ? {} : search),
          }
        : undefined;

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

                onChange({
                    value: val.length ? toJS(val) : undefined,
                    operator: val.length
                        ? (operator ??
                          selectColumnMenuFilterOperators.getContains({
                              valueSelector,
                              rowValueSelector,
                          }))
                        : '',
                    syntheticEvent: event,
                });
            };

            return (
                <SelectorAsync
                    selected={value ?? []}
                    itemComponent={SelectorItemMultiple}
                    onChange={handleChange}
                    search={searchOptions}
                    renderItem={renderer}
                    dataFetcher={dataFetcher}
                    data={data}
                    valueSelector={valueSelector}
                />
            );
        };

        return renderCustomColumnMenuFilter(FilterCell, opts);
    }

    const FilterCell = ({ value, onChange }: TableFilterCellPropsTyped<TO>) => {
        const handleChange = (option: TO, _: boolean, event: SyntheticEvent<HTMLInputElement>) => {
            onChange({
                value: toJS(option),
                operator:
                    operator ??
                    selectColumnMenuFilterOperators.getEquals({ valueSelector, rowValueSelector }),
                syntheticEvent: event,
            });
        };

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

    return renderCustomColumnMenuFilter(FilterCell, opts);
}

export { selectColumnMenuFilterOperators };
