/* eslint-disable complexity */
import React from 'react';

import type {Config, DashKit} from '@gravity-ui/dashkit';
import {ChevronDown, TriangleExclamationFill} from '@gravity-ui/icons';
import type {SelectOption} from '@gravity-ui/uikit';
import {Button, Dialog, DropdownMenu, Icon, Popup, Select, Skeleton} from '@gravity-ui/uikit';
import block from 'bem-cn-lite';
import DialogManager from 'components/DialogManager/DialogManager';
import {I18n} from 'i18n';
import intersection from 'lodash/intersection';
import isEmpty from 'lodash/isEmpty';
import isEqual from 'lodash/isEqual';
import {useDispatch, useSelector} from 'react-redux';
import type {DashTab, DashTabAliases, DashTabItem} from 'shared';
import {DashCommonQa, DashTabItemType} from 'shared';
import {selectDebugMode} from 'store/selectors/user';
import {SelectOptionWithIcon} from 'ui/components/SelectComponents/components/SelectOptionWithIcon/SelectOptionWithIcon';
import type {CurrentRequestState} from 'ui/components/Widgets/Chart/types';
import type {LoadHiddenWidgetMetaType} from 'ui/units/dash/contexts/WidgetMetaContext';
import {selectCurrentTabId} from 'ui/units/dash/store/selectors/dashTypedSelectors';

import {openDialogAliases} from '../../units/dash/store/actions/relations/actions';
import {PlaceholderIllustration} from '../PlaceholderIllustration/PlaceholderIllustration';

import {Content} from './components/Content/Content';
import {AliasesInvalidList} from './components/DialogAliases/components/AliasesList/AliasesInvalidList';
import type {FiltersTypes} from './components/Filters/Filters';
import {Filters} from './components/Filters/Filters';
import {DEFAULT_ALIAS_NAMESPACE, RELATION_TYPES} from './constants';
import {
    getInitialSubItemId,
    getPairedRelationType,
    getRelationsForSave,
    getRelationsIcon,
    getUpdatedPreparedRelations,
    getWidgetsOptions,
    hasConnectionsBy,
} from './helpers';
import {useFilteredRelations} from './hooks/useFilteredRelations';
import {useRelations} from './hooks/useRelations';
import type {ClickCallbackArgs, RelationType, RelationTypeChangeProps, WidgetsTypes} from './types';

import './DialogRelations.scss';

const b = block('dialog-relations');
const i18n = I18n.keyset('component.dialog-relations.view');

const ICON_SIZE = 16;

export const DIALOG_RELATIONS = Symbol('dash/DIALOG_RELATIONS');

export type DialogRelationsProps = {
    onClose: () => void;
    onApply: (item: {aliases?: DashTab['aliases']; connections?: Config['connections']}) => void;
    widget?: DashTabItem;
    allWidgets?: DashTabItem[];
    dashKitRef: React.RefObject<DashKit>;
    dashTabAliases: DashTabAliases | null;
    workbookId: string | null;
    widgetsCurrentTab: Record<string, string>;
    loadHiddenWidgetMeta?: LoadHiddenWidgetMetaType;
};

export type OpenDialogRelationsArgs = {
    id: typeof DIALOG_RELATIONS;
    props: DialogRelationsProps;
};

const renderOptions = (option: SelectOption, silentFetchingWidgets: Set<string>) => {
    if (silentFetchingWidgets.has(option.value)) {
        const icon = <Skeleton className={b('skeleton-option-icon')} />;
        return <SelectOptionWithIcon option={{...option, data: {...option.data, icon}}} />;
    }

    return <SelectOptionWithIcon option={option} />;
};

const EmptyState: React.FC = () => {
    return (
        <PlaceholderIllustration
            className={b('empty-state')}
            size={'m'}
            direction={'column'}
            name={'emptyDirectory'}
            title={i18n('title_empty-selected-widgets')}
        />
    );
};

export const DialogRelations = (props: DialogRelationsProps) => {
    const {
        dashKitRef,
        dashTabAliases,
        workbookId,
        widgetsCurrentTab,
        allWidgets: widgets,
        onClose,
        onApply,
        loadHiddenWidgetMeta,
    } = props;
    const dispatch = useDispatch();
    const showDebugInfo = useSelector(selectDebugMode);
    const tabId = useSelector(selectCurrentTabId);

    const aliasWarnButtonRef = React.useRef<HTMLButtonElement | null>(null);
    const silentRequestCancellationRef = React.useRef<CurrentRequestState>({});

    const [aliasWarnPopupOpen, setAliasWarnPopupOpen] = React.useState(false);
    const [searchValue, setSearchValue] = React.useState('');
    const [typeValues, setTypeValues] = React.useState<Array<FiltersTypes>>([]);

    const [aliases, setAliases] = React.useState(dashTabAliases || {});

    // used for control in group or tab of widget
    const [selectedSubItemId, setSelectedSubItemId] = React.useState(
        getInitialSubItemId(props.widget ?? null, widgetsCurrentTab),
    );

    const {
        isLoading,
        currentWidgetMeta,
        relations,
        datasets,
        dashWidgetsMeta,
        invalidAliases,
        onLoadMeta,
        setRelations,
        currentWidget,
        onChangeCurrentWidget,
        setInvalidAliases,
        silentFetchingWidgets,
    } = useRelations({
        dashKitRef,
        initialWidget: props.widget ?? null,
        dialogAliases: aliases,
        workbookId,
        selectedSubItemId,
        loadHiddenWidgetMeta,
        silentRequestCancellationRef,
    });

    const currentWidgetId = selectedSubItemId || currentWidgetMeta?.widgetId || '';

    const isContentLoading =
        isLoading || silentFetchingWidgets.has(selectedSubItemId || currentWidgetId);

    const widgetsIconMap = React.useMemo(() => {
        const iconsMap: Record<string, JSX.Element | null> = {};

        dashWidgetsMeta?.forEach((widgetMeta) => {
            iconsMap[widgetMeta.widgetId] = getRelationsIcon(widgetMeta, b('relations-icon'));
        });

        return iconsMap;
    }, [dashWidgetsMeta]);

    const widgetOptions = React.useMemo(() => {
        return getWidgetsOptions({tabId, widgetsIconMap, widgets, showDebugInfo});
    }, [tabId, widgetsIconMap, widgets, showDebugInfo]);

    const [changedWidgets, setChangedWidgets] = React.useState<WidgetsTypes>({});

    const {filteredRelations} = useFilteredRelations({
        relations,
        searchValue,
        typeValues,
        changedWidgets,
        currentWidgetId,
    });

    const handleItemChange = (value: string[]) => {
        const newWidgetData = widgetOptions.find((item) => item.value === value[0])?.data;
        // if it's tab of widget or item in group control, widgetId is in the option
        // data, for old controls it's value[0]
        const selectedWidgetId = newWidgetData?.widgetId || value[0];
        const selectedItemId = value[0];

        const newCurrentWidget = widgets?.find(
            (item) => item.id === selectedWidgetId,
        ) as DashTabItem;

        const newSelectedSubItemId = newWidgetData?.isItem ? selectedItemId : null;

        setSelectedSubItemId(newSelectedSubItemId);

        if (!changedWidgets[selectedWidgetId]) {
            const updatedChangedWidgets = {...changedWidgets};
            updatedChangedWidgets[selectedWidgetId] = {};
            setChangedWidgets(updatedChangedWidgets);
        }

        onChangeCurrentWidget({newCurrentWidget, newSelectedSubItemId});
    };

    const handleFilterInputChange = React.useCallback((data: string) => {
        setSearchValue(data);
    }, []);

    const handleFilterTypesChange = React.useCallback((data: Array<FiltersTypes>) => {
        setTypeValues(data);
    }, []);

    /**
     * Update local relations when aliases was added or removed
     */
    const handleUpdateRelations = React.useCallback(
        (changedAliases: string[][]) => {
            if (relations.length === 0) {
                return;
            }

            const relationsWithChangedAliases = relations.map((widgetItem) => {
                const byAliases: Array<Array<string>> = changedAliases.filter((aliasArr) => {
                    if (!widgetItem.usedParams?.length) {
                        return false;
                    }
                    return intersection(widgetItem.usedParams, aliasArr);
                });

                return {
                    ...widgetItem,
                    relations: {
                        ...widgetItem.relations,
                        byAliases,
                    },
                };
            });

            if (DEFAULT_ALIAS_NAMESPACE in aliases) {
                const newAliases = Object.assign({
                    ...aliases,
                    [DEFAULT_ALIAS_NAMESPACE]: changedAliases,
                });
                setAliases(newAliases);
            }
            setRelations(relationsWithChangedAliases);
        },
        [aliases, relations, setRelations],
    );

    /**
     * Update aliases and local relations after apply button click in aliases popup
     */
    const handleUpdateAliases = React.useCallback(
        (newNamespacedAliases) => {
            const newAliases = {
                ...aliases,
                [DEFAULT_ALIAS_NAMESPACE]: newNamespacedAliases,
            };
            setAliases(newAliases);
            handleUpdateRelations(newNamespacedAliases);

            const newPreparedRelations = getUpdatedPreparedRelations({
                aliases: newAliases,
                currentWidgetMeta,
                changedWidgetsData: changedWidgets,
                dashkitData: dashKitRef.current || null,
                dashWidgetsMeta,
                relations,
                datasets,
                currentWidgetId,
                type: 'aliases',
            });
            if (!newPreparedRelations) {
                return;
            }

            const newChangedWidgets = {...changedWidgets};

            newPreparedRelations.forEach((item) => {
                if (
                    changedWidgets &&
                    changedWidgets[currentWidgetId] &&
                    item.widgetId in changedWidgets[currentWidgetId] &&
                    item.relations.type !== changedWidgets[currentWidgetId][item.widgetId]
                ) {
                    delete newChangedWidgets[item.widgetId];
                }
            });
            setChangedWidgets(newChangedWidgets);
            setRelations(newPreparedRelations);
        },
        [
            aliases,
            handleUpdateRelations,
            currentWidgetMeta,
            changedWidgets,
            dashKitRef,
            dashWidgetsMeta,
            relations,
            datasets,
            currentWidgetId,
            setRelations,
        ],
    );

    /**
     * Callback on close alias popup (triggers on Apply button or Cancel button).
     * If Cancel button was clicked (reset arg), then do nothing.
     * If Apply was clicked (changedWidgetsData, changedWidgetId, aliases, etc), then need to recalculate relations info
     */
    const handleAliasesClosed = React.useCallback((args: ClickCallbackArgs) => {
        if (args?.reset || !args?.changedWidgetsData) {
            return;
        }

        setChangedWidgets(args.changedWidgetsData);
    }, []);

    /**
     * Open aliases dialog (shows list of aliases, detailed info, add alias form)
     */
    const handleAliasesClick = React.useCallback(
        (data) => {
            dispatch(
                openDialogAliases({
                    relations: filteredRelations,
                    currentWidget: currentWidgetMeta,
                    datasets,
                    updateRelations: handleUpdateRelations,
                    updateAliases: handleUpdateAliases,
                    onCloseCallback: handleAliasesClosed,
                    forceAddAlias: false,
                    invalidAliases,
                    ...data,
                    dialogAliases: aliases,
                }),
            );
        },
        [
            dispatch,
            handleUpdateAliases,
            handleUpdateRelations,
            datasets,
            filteredRelations,
            currentWidgetMeta,
            handleAliasesClosed,
            invalidAliases,
            aliases,
        ],
    );

    /**
     * Triggers when changed relation type on relations list row.
     * Check if we need force opening aliases' dialog, open if needed.
     */
    const handleRelationTypeChange = React.useCallback(
        (changedData: RelationTypeChangeProps) => {
            const {type, widgetId, forceAddAlias, itemId: subItemId, ...rest} = changedData;

            const newChanged = {...changedWidgets};
            if (!newChanged[currentWidgetId]) {
                newChanged[currentWidgetId] = {};
            }
            let currentRelations;
            if (subItemId) {
                currentRelations = relations.find((item) => item.itemId === subItemId)?.relations;
            } else {
                currentRelations = relations.find((item) => item.widgetId === widgetId)?.relations;
            }

            const relationSubjectId = subItemId || widgetId;
            const currentRelationType = currentRelations?.type;
            if (currentRelationType === type) {
                if (
                    newChanged[currentWidgetId][relationSubjectId] ||
                    newChanged[relationSubjectId]?.[currentWidgetId]
                ) {
                    setChangedWidgets(newChanged);
                    delete newChanged[currentWidgetId][relationSubjectId];
                    delete newChanged[relationSubjectId][currentWidgetId];
                }
            } else {
                newChanged[currentWidgetId][relationSubjectId] = type;
                const pairedType = getPairedRelationType(type);
                if (pairedType !== RELATION_TYPES.unknown) {
                    newChanged[relationSubjectId] = newChanged[relationSubjectId]
                        ? {...newChanged[relationSubjectId], [currentWidgetId]: pairedType}
                        : {[currentWidgetId]: pairedType};
                }
            }
            const changeFromUnknown = currentRelationType === RELATION_TYPES.unknown;

            const showAddAliasForm =
                type !== RELATION_TYPES.ignore &&
                ((changeFromUnknown && forceAddAlias) ||
                    (!changeFromUnknown &&
                        currentRelationType === RELATION_TYPES.ignore &&
                        type !== RELATION_TYPES.unknown) ||
                    forceAddAlias);

            if (showAddAliasForm) {
                if (!isEmpty(newChanged[currentWidgetId][relationSubjectId])) {
                    const hasRelationBy = hasConnectionsBy(currentRelations);

                    if (hasRelationBy && !forceAddAlias) {
                        setChangedWidgets(newChanged);
                    } else {
                        // if there is no native relation then open aliases popup
                        // except widgets with errors
                        handleAliasesClick({
                            ...rest,
                            forceAddAlias: true,
                            changedWidgetsData: newChanged,
                            changedWidgetId: widgetId,
                            changedItemId: subItemId,
                        });
                    }
                }
            } else {
                setChangedWidgets(newChanged);
            }
        },
        [changedWidgets, currentWidgetId, relations, handleAliasesClick],
    );

    /**
     * Clear invalid aliases, which don't below to any dash tab field
     */
    const handleInvalidAliasesClear = React.useCallback(() => {
        const filteredAliases = aliases[DEFAULT_ALIAS_NAMESPACE].map((aliasRow: string[]) => {
            return aliasRow.filter((item) => !invalidAliases?.includes(item));
        }).filter((item: string[]) => item.length > 1);

        handleUpdateAliases(filteredAliases);
        setInvalidAliases([]);
    }, [aliases, handleUpdateAliases, invalidAliases, setInvalidAliases]);

    const handleDisconnectAll = React.useCallback(
        (disconnectType: 'all' | 'charts' | 'selectors') => {
            const newChangedWidgets = {...changedWidgets};
            if (!newChangedWidgets[currentWidgetId]) {
                newChangedWidgets[currentWidgetId] = {};
            }
            const filteredIds = filteredRelations.reduce((res: Record<string, string>, item) => {
                const widgetId = item.itemId || item.widgetId;

                if (!widgetId) {
                    return res;
                }

                const isControl =
                    item.type === DashTabItemType.Control ||
                    item.type === DashTabItemType.GroupControl;

                if (
                    disconnectType === 'all' ||
                    (disconnectType === 'selectors' && isControl) ||
                    (disconnectType === 'charts' && !isControl)
                ) {
                    res[widgetId] = widgetId;

                    if (!newChangedWidgets[widgetId]) {
                        newChangedWidgets[widgetId] = {};
                    }
                }

                return res;
            }, {});

            relations.forEach((item) => {
                const widgetId = item.itemId || item.widgetId;

                if (filteredIds[widgetId]) {
                    newChangedWidgets[currentWidgetId][widgetId] =
                        RELATION_TYPES.ignore as RelationType;
                    newChangedWidgets[widgetId][currentWidgetId] =
                        RELATION_TYPES.ignore as RelationType;
                }
            });

            setChangedWidgets(newChangedWidgets);
        },
        [changedWidgets, currentWidgetId, filteredRelations, relations],
    );

    /**
     * Triggers when click Apply button in relations dialog (saves in store and closes popup)
     */
    const handleSaveRelations = React.useCallback(() => {
        if (!dashKitRef.current) {
            return;
        }
        if (isLoading) {
            onClose();
        }
        const newData: {aliases?: DashTab['aliases']; connections?: Config['connections']} = {};
        if (changedWidgets) {
            const connections = getRelationsForSave({
                changed: changedWidgets,
                dashkitData: dashKitRef.current || null,
            });
            if (connections) {
                newData.connections = connections;
            }
        }

        if (!isEqual(aliases, dashTabAliases || {})) {
            newData.aliases = isEmpty(aliases?.[DEFAULT_ALIAS_NAMESPACE]) ? {} : aliases;
        }

        if (isEmpty(newData)) {
            onClose();
        } else {
            onApply(newData);
        }
    }, [dashKitRef, aliases, isLoading, dashTabAliases, changedWidgets, onClose, onApply]);

    const handleAliasesWarnClick = () => setAliasWarnPopupOpen(!aliasWarnPopupOpen);

    // disable disconnect button when loading
    // when selected only 'none' filter
    const isDisconnectDisabled = Boolean(
        isContentLoading ||
            (typeValues.length === 1 && typeValues[0] === 'none') ||
            !filteredRelations.length,
    );

    // Cancel all silent requests on unmount
    React.useEffect(() => {
        return () => {
            Object.values(silentRequestCancellationRef.current).forEach((requestState) => {
                if (requestState.status === 'loading' && requestState.requestCancellation) {
                    requestState.status = 'canceled';
                    requestState.requestCancellation.cancel();
                }
            });
        };
    }, []);

    return (
        <Dialog
            onClose={onClose}
            open={true}
            className={b()}
            onEnterKeyDown={handleSaveRelations}
            disableOutsideClick={true}
            disableEscapeKeyDown={true}
        >
            <Dialog.Header caption={i18n('title_links')} />
            <Dialog.Body className={b('container')}>
                <Select
                    qa={DashCommonQa.RelationsWidgetSelect}
                    placeholder={i18n('label_empty-state')}
                    className={b('item-select', {
                        'empty-state': !currentWidget,
                    })}
                    popupClassName={b('item-select-popup')}
                    value={currentWidgetId ? [currentWidgetId] : undefined}
                    options={widgetOptions}
                    onUpdate={handleItemChange}
                    filterable={true}
                    disabled={isLoading}
                    renderOption={(option) => renderOptions(option, silentFetchingWidgets)}
                    renderSelectedOption={(option) => renderOptions(option, silentFetchingWidgets)}
                    popupWidth="fit"
                />
                {currentWidget ? (
                    <React.Fragment>
                        <Filters
                            onChangeInput={handleFilterInputChange}
                            onChangeButtons={handleFilterTypesChange}
                        />
                        <Content
                            relations={filteredRelations}
                            widgetMeta={currentWidgetMeta}
                            isLoading={isContentLoading}
                            onChange={handleRelationTypeChange}
                            onAliasClick={handleAliasesClick}
                            showDebugInfo={showDebugInfo}
                            widgetIcon={widgetsIconMap[currentWidgetMeta?.widgetId || '']}
                            onLoadMeta={onLoadMeta}
                            silentFetchingWidgets={silentFetchingWidgets}
                        />
                    </React.Fragment>
                ) : (
                    <EmptyState />
                )}
            </Dialog.Body>
            <Dialog.Footer
                preset="default"
                showError={false}
                textButtonCancel={i18n('button_cancel')}
                textButtonApply={i18n('button_apply')}
                propsButtonCancel={{view: 'outlined', qa: DashCommonQa.RelationsCancelBtn}}
                onClickButtonApply={handleSaveRelations}
                onClickButtonCancel={onClose}
                propsButtonApply={{
                    qa: DashCommonQa.RelationsApplyBtn,
                }}
            >
                {currentWidget && (
                    <React.Fragment>
                        <span className={b('disconnect-text')}>{i18n('button_disconnect')}</span>
                        <DropdownMenu
                            disabled={isDisconnectDisabled}
                            size="l"
                            items={[
                                {
                                    action: () => handleDisconnectAll('all'),
                                    text: i18n('label_all'),
                                    qa: DashCommonQa.RelationsDisconnectAllWidgets,
                                },
                                {
                                    action: () => handleDisconnectAll('charts'),
                                    text: i18n('label_charts'),
                                    qa: DashCommonQa.RelationsDisconnectAllCharts,
                                },
                                {
                                    action: () => handleDisconnectAll('selectors'),
                                    text: i18n('label_selectors'),
                                    qa: DashCommonQa.RelationsDisconnectAllSelectors,
                                },
                            ]}
                            renderSwitcher={({onClick, onKeyDown}) => (
                                <Button
                                    className={b('switcher-button')}
                                    view="normal"
                                    qa={DashCommonQa.RelationsDisconnectAllSwitcher}
                                    disabled={isDisconnectDisabled}
                                    onClick={onClick}
                                    onKeyDown={onKeyDown}
                                >
                                    <Icon
                                        className={b('switcher-button-icon')}
                                        data={ChevronDown}
                                        size={ICON_SIZE}
                                    />
                                </Button>
                            )}
                        />
                        {Boolean(invalidAliases?.length) && (
                            <React.Fragment>
                                <Button
                                    ref={aliasWarnButtonRef}
                                    className={b('error-button')}
                                    onClick={handleAliasesWarnClick}
                                    view="flat"
                                >
                                    <Icon
                                        data={TriangleExclamationFill}
                                        className={b('error-icon')}
                                    />
                                </Button>
                                <Popup
                                    hasArrow={true}
                                    anchorElement={aliasWarnButtonRef.current}
                                    open={aliasWarnPopupOpen}
                                    placement="right"
                                    className={b('invalid-list-popup')}
                                >
                                    <div className={b('warn-content')}>
                                        <div className={b('warn-title')}>
                                            {i18n('label_invalid-alias-title')}
                                        </div>
                                        <div className={b('warn-text')}>
                                            {i18n('label_invalid-alias-text')}
                                        </div>
                                        <AliasesInvalidList
                                            aliases={aliases?.[DEFAULT_ALIAS_NAMESPACE]}
                                            invalidAliases={invalidAliases}
                                            datasets={datasets}
                                            onClose={handleAliasesWarnClick}
                                            onClear={handleInvalidAliasesClear}
                                        />
                                    </div>
                                </Popup>
                            </React.Fragment>
                        )}
                    </React.Fragment>
                )}
            </Dialog.Footer>
        </Dialog>
    );
};

DialogManager.registerDialog(DIALOG_RELATIONS, DialogRelations);
