import {useCallback, useEffect, useMemo, useRef, useState} from "@wordpress/element";
import {__, sprintf} from "@wordpress/i18n";
import {handleError, handleWarning} from "../../../../../../../helpers/notifications";
import {handlePaginationAfterDeletion} from "../../../../../../../helpers";
import {useBackgroundProcess} from "../../../../../../contexts/background-process";
import AddEdit from "../add-edit";
import Checkbox from "../../../../../../fields/checkbox";
import Footer from "../footer";
import {GetJobs, GetJob} from "../../../../../../../rest/jobs";
import Table from "../../../../../../sections/table";
import Empty from "../../../../containers/content/empty";
import ContentLoading from "../../../../containers/content/loading";
import ContentBodyInner from "../../../../containers/content/body-inner";
import Container from "../../../../containers/content/container";
import confirm from "../../../../../../../helpers/confirm";
import useUnsavedChangesWarning from "../../../../../components/unsaved-changes-warning";
import {
    DATASET_STATUS_INDEXED,
    DATASET_TYPE_ACTIONABLE_KNOWLEDGE,
    GetDatasets,
    GetDataset
} from "../../../../../../../rest/datasets";
import {parseDateString} from "../../../../../../../../helpers";
import {renderName, renderSource, renderStorage, renderAIProvider} from "../_renderers";
import RealTimeRelativeTime from "../../../../../../fields/real-time-relative-time";
import {
    DGEN_PROCESS_KEY_PREFIX,
    DIN_PROCESS_KEY_PREFIX
} from "../../../../../../../data/bg-processes";
import {DATASET_PROCESSES_INFO} from "../_data";
import DatasetRowActions from "../row-actions";
import {
    NOTIF_AUTOCLOSE_TIMEOUT,
    NOTIF_QUICK_AUTOCLOSE_TIMEOUT,
    NOTIF_ERROR_AUTOCLOSE_TIMEOUT
} from "../../../../../../containers/notification-layout";
import Chip from "../../../../../../fields/chip";
import {GetSettings} from "../../../../../../../rest/settings";
import {GetModel} from "../../../../../../../rest/models";
import {SETTINGS_UTILITIES_KEY_PREFIX} from "../../../../../../../data/settings";

/**
 * Check is all selected
 *
 * @param {number[]|string[]} selectedItems Selected items IDs (can include "all")
 * @param {object[]} datasets Datasets
 * @return {boolean}
 */
const checkIsAllSelected = (selectedItems, datasets) => {
    // Handle "all" state
    if (selectedItems.length === 1 && selectedItems[0] === "all") {
        return true;
    }

    const selected = datasets.every(item => selectedItems.includes(item.id));
    let all = selectedItems.length > 0;
    if (datasets.length > 0) {
        all &&= selected;
    }

    return all;
}

/**
 * Check is partially selected
 *
 * @param {number[]|string[]} selectedItems Selected items IDs (can include "all")
 * @param {object[]} datasets Datasets
 * @return {boolean}
 */
const checkIsPartiallySelected = (selectedItems, datasets) => {
    // "all" state is not considered partial
    if (selectedItems.length === 1 && selectedItems[0] === "all") {
        return false;
    }
    return datasets.some(item => selectedItems.includes(item.id));
}

/**
 * Is row disabled
 *
 * @param {object} row Row data
 * @param {number} index Row index
 * @param {object} processState Process state
 * @return {boolean}
 */
const isRowDisabled = (row, index, processState) => {
    // Case 1: Row is part of ongoing dataset_sync or dataset_delete process
    if (processState?.datasetIds?.includes(row.id)) {
        return true;
    }

    // Case 2: If the row isn't yet indexed, it is still invalid
    if (row.status !== DATASET_STATUS_INDEXED) {
        return true;
    }

    return false;
}

/**
 * Check if notifications have errors
 *
 * @param {object} notifications Notifications context
 * @return {boolean}
 */
const hasNotificationErrors = (notifications) => {
    return notifications.get.some(notification => notification.type === 'error');
}

export default function KnowledgeRecommendations({notifications}) {
    // Get background process context
    const {
        // Process state
        processState,

        // Progress states
        generatingProgress,
        syncingProgress,
        deletingProgress,

        // Loading states
        saving,
        pausing,
        canceling,
        preparing,
        syncing,
        deleting,

        // Functions from context
        checkActiveProcess,
        pauseProcess,
        resumeProcess,
        cancelProcess,
        setProcessState,
        createJob,
        followJob,

        // Setters from context
        setGeneratingProgress,
        setSyncingProgress,
        setDeletingProgress,
        setSaving,
        setPausing,
        setCanceling,
        setPreparing,
        setSyncing,
        setDeleting,
    } = useBackgroundProcess();

    // Local component states
    const [isDataFetched, setIsDataFetched] = useState(false);
    const [showLoading, setShowLoading] = useState(false);
    const [loading, setLoading] = useState(0);
    const [formData, setFormData] = useState(null);
    const [addingEditing, setAddingEditing] = useState(false);
    const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);
    // Data
    const giFormRef = useRef(null);
    const [datasetsPagination, setDatasetsPagination] = useState({
        page: 1,
        perPage: LimbChatbot.screen_options?.lbaic_items_per_page || 10,
        total: 0
    });
    const [datasetsOrder, setDatasetsOrder] = useState({
        orderBy: 'updated_at',
        order: 'desc',
    });
    const [datasets, setDatasets] = useState([]);
    const [selectedDSItems, setSelectedDSItems] = useState([]);
    const [kbAIProviderId, setKbAIProviderId] = useState(null);
    const [kbDimension, setKbDimension] = useState(null);

    // Check unsaved changes before leaving the page
    useUnsavedChangesWarning((formData?.id && hasUnsavedChanges) || saving);

    /**
     * Check page query params for action=add and post_type
     * This runs once on mount to handle navigation from actions page
     */
    useEffect(() => {
        const searchParams = new URLSearchParams(window.location.search);
        const action = searchParams.get('action');
        const postType = searchParams.get('post_type');

        // If action=add, open the AddEdit form
        if (action === 'add') {
            // Prepare form data with post_type if provided
            const initialFormData = postType ? {
                generating: {
                    post_type: postType
                }
            } : null;

            setFormData(initialFormData);
            toggleGIForm(true);

            // Clean up URL params after processing
            const url = new URL(window.location);
            url.searchParams.delete('action');
            url.searchParams.delete('post_type');
            window.history.replaceState({}, '', url);
        }
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    /**
     * Auto close notifications
     */
    const autoCloseNotifications = useCallback((timeout = NOTIF_AUTOCLOSE_TIMEOUT) => {
        notifications.set(prevState => prevState.map(item => ({
            ...item,
            autoClose: true,
            timeout,
        })));
    }, [notifications]);


    /**
     * Fetch chatbot AI provider setting
     */
    const fetchKnowledgeSettingsAIProvider = useCallback(async () => {
        try {
            const settings = await GetSettings(
                LimbChatbot.rest.url,
                LimbChatbot.rest.nonce,
                {
                    key: [
                        `${SETTINGS_UTILITIES_KEY_PREFIX}chatbot.kb_ai_model_id`,
                        `${SETTINGS_UTILITIES_KEY_PREFIX}embedding.default.ai_provider_id`,
                        `${SETTINGS_UTILITIES_KEY_PREFIX}embedding.default.dimension`
                    ],
                }
            );

            // Check knowledge AI model
            const kbAIModelId = settings?.find(setting => setting.key === `${SETTINGS_UTILITIES_KEY_PREFIX}chatbot.kb_ai_model_id`)?.value;

            if (kbAIModelId) {
                const model = await GetModel(LimbChatbot.rest.url, LimbChatbot.rest.nonce, kbAIModelId, {include: ['metas']});

                if (model?.id) {
                    setKbAIProviderId(model.ai_provider_id);

                    // Try to get dimension from model metas first
                    if (model.metas && Array.isArray(model.metas)) {
                        const dimensionMeta = model.metas.find(meta => meta.meta_key === 'default_dimension');
                        if (dimensionMeta?.meta_value) {
                            const modelDimension = parseInt(dimensionMeta.meta_value, 10);
                            setKbDimension(modelDimension);
                        }
                    }
                    return;
                }
            }

            // Fallback: Check embedding utility default dimension from settings
            const embeddingDimension = settings?.find(setting => setting.key === `${SETTINGS_UTILITIES_KEY_PREFIX}embedding.default.dimension`)?.value;

            if (embeddingDimension) {
                setKbDimension(parseInt(embeddingDimension, 10));
            }

            // Check embedding utility default ai provider
            const embeddingAIProviderId = settings?.find(setting => setting.key === `${SETTINGS_UTILITIES_KEY_PREFIX}embedding.default.ai_provider_id`)?.value;

            if (embeddingAIProviderId) {
                setKbAIProviderId(embeddingAIProviderId);
            }
        } catch (e) {
            // Handle error silently or log it
            handleError(e);
        }
    }, []);

    useEffect(() => {
        // Hide all permanent notifications
        autoCloseNotifications(NOTIF_QUICK_AUTOCLOSE_TIMEOUT);
        // Fetch chatbot AI provider
        fetchKnowledgeSettingsAIProvider();
        // Check for ongoing processes
        checkForOngoingProcesses();

        return () => {
            // Hide all permanent notifications
            autoCloseNotifications(NOTIF_QUICK_AUTOCLOSE_TIMEOUT)
        }
    }, []);

    useEffect(() => {
        // Fetch when order changes
        fetchDSItems(1, datasetsPagination.perPage, datasetsOrder);
    }, [datasetsOrder]);

    // Memoize computed selection states
    const isAllSelected = useMemo(() =>
            checkIsAllSelected(selectedDSItems, datasets),
        [selectedDSItems, datasets]
    );
    const isPartiallySelected = useMemo(() =>
            !isAllSelected && checkIsPartiallySelected(selectedDSItems, datasets),
        [isAllSelected, selectedDSItems, datasets]
    );

    // Check if all items on current page are selected (not including "all" state)
    const isAllPageSelected = useMemo(() => {
        if (selectedDSItems.length === 1 && selectedDSItems[0] === "all") {
            return true;
        }
        if (datasets.length === 0) {
            return false;
        }
        return datasets.every(item => selectedDSItems.includes(item.id));
    }, [selectedDSItems, datasets]);

    // Current page items count
    const currentPageItemsCount = useMemo(() => {
        return datasets.length;
    }, [datasets]);

    // Auto-convert to "all" state when all page items are selected and total equals page items
    useEffect(() => {
        const isAllState = selectedDSItems.length === 1 && selectedDSItems[0] === "all";
        if (isAllPageSelected && !isAllState) {
            const totalCount = datasetsPagination.total;
            if (totalCount === currentPageItemsCount && currentPageItemsCount > 0) {
                // All items are selected and there are no more items beyond current page
                // Automatically set to "all" state for consistency
                setSelectedDSItems(["all"]);
            }
        }
    }, [isAllPageSelected, selectedDSItems.length, datasetsPagination.total, currentPageItemsCount]);

    /**
     * Generic function to run any dataset process
     * Creates a job and follows it
     *
     * @param {string} processType Process type (dataset_generating, dataset_sync, dataset_delete)
     * @param {number[]} ids Dataset IDs (optional for form-based processes)
     * @param {object} jobConfig Job configuration object
     * @param {function} followFunction Function to follow the process
     * @param {function} setLoadingFunction Function to set loading state
     * @param {function} cleanupFunction Function to clean up after process
     * @return {Promise<void>}
     */
    const runDatasetProcess = useCallback(async (processType, ids = null, jobConfig = null, followFunction, setLoadingFunction, cleanupFunction = null) => {
        // Check if process is active
        if (!checkActiveProcess(processType)) {
            return;
        }

        // Set loading state
        setLoadingFunction(ids ? [...ids] : true);

        try {
            // Set preparing state to disable all buttons
            setPreparing(true);
            // Create job
            const jobData = {
                type: processType,
                sub_type: 'actionable', // Recommendations page
                config: jobConfig || {},
                chatbot_uuid: 'default'
            };

            // Add dataset_ids for dataset-based processes
            if (ids && ids.length > 0) {
                // Convert ["all"] to string "all" for dataset_ids
                if (ids.length === 1 && ids[0] === "all") {
                    jobData.config.dataset_ids = "all";
                } else {
                    jobData.config.dataset_ids = ids;
                }
            }

            const job = await createJob(jobData);

            setProcessState(null);

            // Wait 800ms before starting to follow the process
            setTimeout(async () => {
                setPreparing(false); // Clear preparing state when process actually starts
                await followFunction(job.id, ids);
                if (cleanupFunction) {
                    cleanupFunction();
                }
                setLoadingFunction(ids ? [] : false);
            }, 800);

        } catch (e) {
            setPreparing(false); // Clear preparing state on error
            handleError(e, notifications.set, {
                title: sprintf(__("Failed to start %s process.", 'limb-chatbot'), DATASET_PROCESSES_INFO[processType].title.toLowerCase()),
                description: e.message ? __(e.message, 'limb-chatbot') : __("Please check and try again.", 'limb-chatbot'),
            });
            setLoadingFunction(ids ? [] : false);
        }
    }, [checkActiveProcess, setPreparing, createJob, setProcessState, notifications]);

    /**
     * Toggle generating/indexing form
     *
     * @param {boolean} show Show
     */
    const toggleGIForm = useCallback((show) => {
        setAddingEditing(show);
    }, []);

    /**
     * Cancel form (close the add/edit form)
     */
    const cancelForm = useCallback(() => {
        toggleGIForm(false);
    }, [toggleGIForm]);

    /**
     * Show from to add datasets
     */
    const showAddDatasetsForm = () => {
        setShowLoading(true);
        setFormData(null);
        toggleGIForm(true);
    }

    /**
     * Get datasets
     */
    const fetchDSItems = useCallback(async (page, perPage, params = {}, showLoading = true, settings = null) => {
        showLoading && setLoading(prev => prev + 1);
        try {
            // Request params
            const orderby = params.orderBy || 'updated_at';
            const order = params.order || 'desc';
            const reqParams = {
                page: page,
                per_page: perPage,
                orderby,
                order,
                type: DATASET_TYPE_ACTIONABLE_KNOWLEDGE,
                include: ['metas', 'storage', 'source_object', 'source_url', 'ai_provider_id']
            }
            // Include storage, source_object if not skipped
            if (settings?.skipInclude?.length) {
                reqParams.include = reqParams.include.filter(item => !settings.skipInclude.includes(item));
            }
            // Get datasets
            const res = await GetDatasets(LimbChatbot.rest.url, LimbChatbot.rest.nonce, reqParams);
            // Update datasets state
            if (res.items?.length) {
                if (settings?.skipInclude?.length) {
                    // Merge first-level data with existing once to not lose storage and source_object
                    setDatasets(prevState => {
                        const mergedItems = res.items.map(item => {
                            const found = prevState.find(di => di.id === item.id);
                            if (found) {
                                return {
                                    ...found,
                                    ...item
                                };
                            }

                            return item;
                        });

                        // Check if there are new items without storage that need to be fetched
                        const needsStorageFetch = settings.skipInclude.includes('storage');
                        if (needsStorageFetch) {
                            const newItemsWithoutStorage = mergedItems.filter(item => {
                                const wasInPrevState = prevState.find(di => di.id === item.id);
                                return !wasInPrevState && !item.storage?.id;
                            });

                            // If there are new items without storage, fetch storage info for them
                            if (newItemsWithoutStorage.length > 0) {
                                // Fetch storage info for new items asynchronously
                                Promise.all(
                                    newItemsWithoutStorage.map(item =>
                                        GetDataset(LimbChatbot.rest.url, LimbChatbot.rest.nonce, item.id, {
                                            include: ['storage']
                                        }).catch(() => null) // Silently fail for individual items
                                    )
                                ).then(storageResults => {
                                    // Filter out null results and extract dataset objects from API response
                                    const validResults = storageResults
                                        .filter(sr => sr !== null && sr?.dataset?.id)
                                        .map(sr => sr.dataset);
                                    if (validResults.length > 0) {
                                        setDatasets(currentState => {
                                            return currentState.map(item => {
                                                // Only update if item still exists and doesn't already have storage
                                                const storageResult = validResults.find(sr => sr?.id === item.id);
                                                if (storageResult?.storage && !item.storage?.id) {
                                                    return {
                                                        ...item,
                                                        storage: storageResult.storage
                                                    };
                                                }
                                                return item;
                                            });
                                        });
                                    }
                                }).catch(() => {
                                    // Silently fail - storage info will be available on next full fetch
                                });
                            }
                        }

                        return mergedItems;
                    });
                } else {
                    setDatasets(res.items);
                }
            } else {
                setDatasets([]);
            }
            // Pagination state
            setDatasetsPagination(prevState => ({
                ...prevState,
                page: page,
                perPage: perPage,
                total: +res.total,
            }));
            // Table ordering state - use functional update to compare with current state, not stale closure
            setDatasetsOrder(prevState => {
                if (prevState.orderBy !== orderby || prevState.order !== order) {
                    return {
                        ...prevState,
                        orderBy: orderby,
                        order,
                    };
                }
                return prevState; // Return same reference to prevent re-render
            });

            const activeProcessDatasetIds = processState?.datasetIds || [];
            setSelectedDSItems(prevState => {
                // Preserve "all" state
                if (prevState.length === 1 && prevState[0] === "all") {
                    return prevState;
                }
                return prevState.filter(item => res.items.findIndex(i => i.id === item) !== -1 || activeProcessDatasetIds.includes(item));
            });
        } catch (e) {
            handleError(e, notifications.set, {
                title: __("Failed to retrieve items data.", 'limb-chatbot'),
                description: e.message ? __(e.message, 'limb-chatbot') : __("Please check your connection and try again.", 'limb-chatbot'),
            });
        }
        setIsDataFetched(true);
        showLoading && setLoading(prev => prev - 1);
    }, [notifications]);

    /**
     * Set bg process form state from job config
     *
     * @param {object} jobConfig Job config object (required)
     * @param {string} processType Process type (for determining which config keys to use)
     * @return {Promise<void>}
     */
    const setBgProcessFormState = async (jobConfig, processType = null) => {
        if (!jobConfig) {
            return;
        }

        try {
            const data = {
                generating: {},
                indexing: {},
            };

            // Extract form data from job config
            // Generating config
            if (jobConfig.source !== undefined) data.generating.source = jobConfig.source;
            if (jobConfig.post_type !== undefined) data.generating.post_type = jobConfig.post_type;
            if (jobConfig.limit !== undefined) data.generating.limit = jobConfig.limit;
            if (jobConfig.files !== undefined) data.generating.files = jobConfig.files;
            if (jobConfig.q_a !== undefined) data.generating.q_a = jobConfig.q_a;
            if (jobConfig.text !== undefined) data.generating.text = jobConfig.text;
            if (jobConfig.ai_model_id !== undefined) data.generating.ai_model_id = jobConfig.ai_model_id;
            if (jobConfig.config_id !== undefined) data.generating.config_id = jobConfig.config_id;
            if (jobConfig.jet_engine_custom_fields !== undefined) {
                data.generating.jet_engine_custom_fields = Array.isArray(jobConfig.jet_engine_custom_fields)
                    ? [...jobConfig.jet_engine_custom_fields]
                    : jobConfig.jet_engine_custom_fields;
            }
            if (jobConfig.jet_engine_meta_box_fields !== undefined) {
                data.generating.jet_engine_meta_box_fields = Array.isArray(jobConfig.jet_engine_meta_box_fields)
                    ? [...jobConfig.jet_engine_meta_box_fields]
                    : jobConfig.jet_engine_meta_box_fields;
            }

            // Indexing config (from indexing_ prefixed keys)
            if (jobConfig.indexing_ai_model_id !== undefined) {
                data.indexing.ai_model_id = jobConfig.indexing_ai_model_id;
            }
            if (jobConfig.indexing_config_id !== undefined) {
                data.indexing.config_id = jobConfig.indexing_config_id;
            }
            if (jobConfig.indexing_dimension !== undefined) {
                data.indexing.dimension = jobConfig.indexing_dimension;
            } else if (jobConfig.dimension !== undefined) {
                data.indexing.dimension = jobConfig.dimension;
            }
            if (jobConfig.indexing_vector_index_type !== undefined) {
                data.indexing.vector_index_type = jobConfig.indexing_vector_index_type;
            } else if (jobConfig.vector_index_type !== undefined) {
                data.indexing.vector_index_type = jobConfig.vector_index_type;
            }
            if (jobConfig.indexing_vector_index_config_id !== undefined) {
                data.indexing.vector_index_config_id = jobConfig.indexing_vector_index_config_id;
            } else if (jobConfig.vector_index_config_id !== undefined) {
                data.indexing.vector_index_config_id = jobConfig.vector_index_config_id;
            }
            if (jobConfig.vector_index_id !== undefined) {
                data.indexing.vector_index_id = jobConfig.vector_index_id;
            }

            setFormData(data);
        } catch (e) {
            handleError(e);
        }
    }

    /**
     * Handle dataset-based processes (sync, delete, recovery)
     */
    const handleDatasetBasedProcess = async (processType, status, datasetIds, startedAt, startedFrom = null, jobId = null) => {
        if (!datasetIds?.length) {
            handleError({
                message: `The ${processType} process isn't complete, but there are no items to continue.`,
                data: datasetIds,
            }, notifications.set, {
                title: sprintf(__("%s process invalid state.", 'limb-chatbot'), DATASET_PROCESSES_INFO[processType].title),
                description: sprintf(__("%s process isn't complete, but there are no items to continue.", 'limb-chatbot'), DATASET_PROCESSES_INFO[processType].title),
            });
            return;
        }

        setSelectedDSItems([...datasetIds]);

        if (status === 'pause') {
            setProcessState({
                type: processType,
                status: 'pause',
                startedAt,
                datasetIds,
                started_from: startedFrom,
                jobId: jobId,
            });
            handleWarning({
                message: `The ${processType} process has been paused.`,
                data: datasetIds
            }, notifications.set, {
                title: sprintf(__("%s process has been paused.", 'limb-chatbot'), DATASET_PROCESSES_INFO[processType].title),
                description: __("Please continue the process to complete.", 'limb-chatbot'),
            });
        } else {
            // Set appropriate state and follow process
            const setterMap = {
                'dataset_sync': setSyncing,
                'dataset_delete': setDeleting,
            };
            const followMap = {
                'dataset_sync': followSyncBackgroundProcess,
                'dataset_delete': followDeleteBackgroundProcess,
            };

            // Set the process state
            setProcessState({
                type: processType,
                status: 'start',
                startedAt,
                datasetIds,
                started_from: startedFrom,
                jobId: jobId,
            });
            setterMap[processType]([...datasetIds]);
            await followMap[processType](jobId, [...datasetIds]);
            setSelectedDSItems([]);
            setterMap[processType]([]);
        }
    };

    /**
     * Handle form-based processes (generating)
     */
    const handleFormBasedProcess = async (processType, status, datasetIds = null, startedAt, startedFrom = null, jobId = null) => {
        // Set state of the form from job config (only if we have a jobId)
        if (jobId) {
            try {
                const job = await GetJob(jobId);
                if (job?.config) {
                    await setBgProcessFormState(job.config, processType);
                }
            } catch (e) {
                handleError(e);
            }
        }
        // Show the form if dataset generating process is going
        if (processType === 'dataset_generating') {
            toggleGIForm(true);
        }

        setProcessState({
            type: processType,
            status,
            startedAt,
            datasetIds,
            started_from: startedFrom,
            jobId: jobId,
        });

        if (status === 'start' && jobId) {
            // Follow the appropriate process
            // Note: saving state is managed by onComplete callbacks, not here
            if (processType === 'dataset_generating') {
                await followDatasetGeneratingBackgroundProcess(jobId);
            }
        }
    };

    /**
     * Check to follow ongoing processes
     * Uses GET /jobs endpoint with sub_type filter
     *
     * @return {Promise<void>}
     */
    const checkForOngoingProcesses = async () => {
        setLoading(prev => prev + 1);
        try {
            // Get jobs for this page (sub_type = 'recommendations')
            const res = await GetJobs({
                chatbot_uuid: 'default',
                sub_type: 'actionable'
            });
            setLoading(prev => prev - 1);

            if (!res?.items?.length) {
                return;
            }

            // Process each active job
            for (const job of res.items) {
                const jobStatus = job.status;
                const jobType = job.type;
                const progress = job.progress_percent || 0;
                const datasetIds = job.config?.dataset_ids || null;
                const startedAt = job.started_at ? parseDateString(job.started_at) : null;

                // Only process jobs that are active or paused
                if (jobStatus !== 'processing' &&
                    jobStatus !== 'generating_tasks' &&
                    jobStatus !== 'pending' &&
                    jobStatus !== 'paused') {
                    continue;
                }

                // Set progress state
                switch (jobType) {
                    case 'dataset_generating':
                        setGeneratingProgress(progress);
                        break;
                    case 'dataset_sync':
                        setSyncingProgress(progress);
                        break;
                    case 'dataset_delete':
                        setDeletingProgress(progress);
                        break;
                }

                // Map job status to process status
                const processStatus = jobStatus === 'paused' ? 'pause' :
                    (jobStatus === 'processing' || jobStatus === 'generating_tasks' || jobStatus === 'pending') ? 'start' : null;

                if (!processStatus) {
                    continue;
                }

                // Handle based on process type
                if (jobType === 'dataset_generating') {
                    // Form-based process
                    await handleFormBasedProcess(jobType, processStatus, datasetIds, startedAt, null, job.id);
                } else {
                    // Dataset-based process
                    await handleDatasetBasedProcess(jobType, processStatus, datasetIds, startedAt, null, job.id);
                }
            }
        } catch (e) {
            setLoading(prev => prev - 1);
            handleError(e);
        }
    }

    /**
     * Check/Uncheck all datasets
     */
    const toggleAll = () => {
        if (processState?.type) {
            return;
        }
        const isAllState = selectedDSItems.length === 1 && selectedDSItems[0] === "all";
        if (isAllSelected || isAllState) {
            setSelectedDSItems([]);
        } else {
            setSelectedDSItems([...datasets.map(item => item.id)]);
        }
    }

    /**
     * Toggle select all / clear selection
     */
    const toggleSelectAll = () => {
        if (processState?.type) {
            return;
        }
        const isAllState = selectedDSItems.length === 1 && selectedDSItems[0] === "all";
        if (isAllState) {
            setSelectedDSItems([]);
        } else {
            setSelectedDSItems(["all"]);
        }
    }

    /**
     * Helper function to convert form settings data to job config
     *
     * @param {object[]} settingsData Settings data from form
     * @return {object} Job config object
     */
    const convertFormDataToJobConfig = useCallback(async (settingsData) => {
        const config = {};

        // Extract generating config
        const generatingSettings = settingsData.filter(item => item.key.startsWith(DGEN_PROCESS_KEY_PREFIX));
        generatingSettings.forEach(setting => {
            const key = setting.key.replace(DGEN_PROCESS_KEY_PREFIX, '');
            config[key] = setting.value;
        });

        // Include jet_engine_custom_fields in job config only when non-empty (array of field keys)
        if (Array.isArray(config.jet_engine_custom_fields) && config.jet_engine_custom_fields.length === 0) {
            delete config.jet_engine_custom_fields;
        }
        // Include jet_engine_meta_box_fields in job config only when non-empty (array of field keys)
        if (Array.isArray(config.jet_engine_meta_box_fields) && config.jet_engine_meta_box_fields.length === 0) {
            delete config.jet_engine_meta_box_fields;
        }

        // Extract indexing config
        const indexingSettings = settingsData.filter(item => item.key.startsWith(DIN_PROCESS_KEY_PREFIX));
        indexingSettings.forEach(setting => {
            const key = setting.key.replace(DIN_PROCESS_KEY_PREFIX, '');
            // Map indexing config keys with indexing_ prefix for generating job
            if (key === 'ai_model_id') {
                config.indexing_ai_model_id = setting.value;
            } else if (key === 'config_id') {
                config.indexing_config_id = setting.value;
            } else if (key === 'dimension') {
                config.indexing_dimension = setting.value;
            } else if (key === 'vector_index_type') {
                config.indexing_vector_index_type = setting.value;
            } else if (key === 'vector_index_config_id') {
                config.indexing_vector_index_config_id = setting.value;
            }
        });

        // Ensure indexing dimension aligns with the selected model default
        if (config.indexing_ai_model_id) {
            try {
                const model = await GetModel(
                    LimbChatbot.rest.url,
                    LimbChatbot.rest.nonce,
                    config.indexing_ai_model_id,
                    {include: ['metas']}
                );

                const dimensionMeta = model?.metas?.find(meta => meta.meta_key === 'default_dimension');
                const modelDimension = dimensionMeta?.meta_value ? parseInt(dimensionMeta.meta_value, 10) : null;

                if (modelDimension) {
                    config.indexing_dimension = modelDimension;
                }
            } catch (e) {
                // Keep existing dimension if model fetch fails, but log the error
                handleError(e);
            }
        }

        return config;
    }, []);

    /**
     * Follow delete background process
     *
     * @param jobId
     * @param {number[]} datasetIds Dataset IDs being processed
     * @param {object} params More params
     * @return {Promise<void>}
     */
    const followDeleteBackgroundProcess = async (jobId, datasetIds, params = null) => {
        await followJob({
            jobId,
            processType: 'dataset_delete',
            setProgressState: (v) => {
                setDeletingProgress(v);
            },
            onComplete: async () => {
                setProcessState(null);
                setDeleting([]);
                // Refresh datasets list to remove deleted items
                await fetchDSItems(datasetsPagination.page, datasetsPagination.perPage, datasetsOrder, false);
                // Handle pagination after deletion
                handlePaginationAfterDeletion({
                    currentItemsCount: datasets.length,
                    deletedCount: datasetIds.length,
                    pagination: datasetsPagination,
                    fetchFunction: fetchDSItems,
                    order: datasetsOrder,
                    setItems: setDatasets,
                    setPagination: setDatasetsPagination,
                    deletedIds: datasetIds
                });
                // Hide all notifications with appropriate timeout based on errors
                const timeout = hasNotificationErrors(notifications) ? NOTIF_ERROR_AUTOCLOSE_TIMEOUT : NOTIF_AUTOCLOSE_TIMEOUT;
                autoCloseNotifications(timeout);
            },
            onProgressUpdate: async () => {
                // Update table data during deletion
                await fetchDSItems(datasetsPagination.page, datasetsPagination.perPage, datasetsOrder, false, {
                    skipInclude: ['storage', 'source_object', 'source_url'],
                });
            },
            resetProgress: () => setDeletingProgress(false),
            statusMessages: {
                complete: DATASET_PROCESSES_INFO['dataset_delete'].success,
                pause: sprintf(__("%s process has been paused.", 'limb-chatbot'), DATASET_PROCESSES_INFO['dataset_delete'].processName),
                cancel: sprintf(__("%s process has been cancelled.", 'limb-chatbot'), DATASET_PROCESSES_INFO['dataset_delete'].processName)
            }
        });
    }

    const runDatasetDelete = async (ids) => {
        if (!ids?.length) {
            return;
        }
        await runDatasetProcess(
            'dataset_delete',
            ids,
            {}, // No additional config needed for delete
            followDeleteBackgroundProcess,
            setDeleting,
            () => setSelectedDSItems([])
        );
    }

    /**
     * Delete dataset
     *
     * @param {object} dataset Dataset
     * @param {number} index Index
     */
    const deleteDSItem = async (dataset, index) => {
        if (await confirm(__("Are you sure you want to delete the item?", 'limb-chatbot'))) {
            // Add to selected items to show footer buttons
            setSelectedDSItems([dataset.id]);
            runDatasetDelete([dataset.id]);
        }
    }

    /**
     * Delete selected datasets
     */
    const deleteSelected = async () => {
        // Check if there's a paused process to resume
        if (processState && processState.status === 'pause') {
            const isDeleteProcess = processState.type === 'dataset_delete';

            if (isDeleteProcess) {
                // Resume the paused delete process (don't create a new job)
                await resumeProcess();
                return;
            }
        }

        // Normal flow: confirm and delete
        if (selectedDSItems.length > 0 && await confirm(__("Are you sure you want to delete the selected ones?", 'limb-chatbot'))) {
            runDatasetDelete(selectedDSItems);
        }
    }

    /**
     * Follow sync background process
     *
     * @param jobId
     * @param {number[]} datasetIds Dataset IDs being processed
     * @param {object} params More params
     * @return {Promise<void>}
     */
    const followSyncBackgroundProcess = async (jobId, datasetIds, params = null) => {
        await followJob({
            jobId,
            processType: 'dataset_sync',
            setProgressState: (v) => {
                setSyncingProgress(v);
            },
            onComplete: async () => {
                setSyncing([]);
            },
            onProgressUpdate: async () => {
                await fetchDSItems(datasetsPagination.page, datasetsPagination.perPage, datasetsOrder, false, {
                    skipInclude: ['storage', 'source_object', 'source_url'],
                });
            },
            resetProgress: () => setSyncingProgress(false),
            statusMessages: {
                complete: DATASET_PROCESSES_INFO['dataset_sync'].success,
                pause: sprintf(__("%s process has been paused.", 'limb-chatbot'), DATASET_PROCESSES_INFO['dataset_sync'].processName),
                cancel: sprintf(__("%s process has been cancelled.", 'limb-chatbot'), DATASET_PROCESSES_INFO['dataset_sync'].processName)
            }
        });
    }

    /**
     * Run sync process
     *
     * @param {number[]} ids Dataset IDs
     * @return {Promise<void>}
     */
    const runDatasetSync = async (ids) => {
        if (!ids?.length) {
            return;
        }
        await runDatasetProcess(
            'dataset_sync',
            ids,
            {}, // No additional config needed for sync
            followSyncBackgroundProcess,
            setSyncing
        );
    }

    /**
     * Sync
     *
     * @param {object} dataset Dataset
     */
    const sync = async (dataset) => {
        // Add to selected items to show footer buttons
        setSelectedDSItems([dataset.id]);
        await runDatasetSync([dataset.id]);
    }

    /**
     * Sync selected datasets
     */
    const syncSelected = async () => {
        // Check if there's a paused process to resume
        if (processState && processState.status === 'pause') {
            const isSyncProcess = processState.type === 'dataset_sync';

            if (isSyncProcess) {
                // Resume the paused sync process (don't create a new job)
                await resumeProcess();
                return;
            }
        }

        // Normal flow: sync selected items
        await runDatasetSync(selectedDSItems);
    }


    /**
     * Follow a generating background process
     *
     * @param jobId
     * @param {object} params More params
     *
     * @return {Promise<void>}
     */
    const followDatasetGeneratingBackgroundProcess = async (jobId, params = null) => {
        await followJob({
            jobId,
            processType: 'dataset_generating',
            setProgressState: (v) => {
                setGeneratingProgress(v);
            },
            onComplete: async () => {
                setSaving(false);
                // Refresh datasets list to show updated state (remove warnings, show correct status)
                await fetchDSItems(1, datasetsPagination.perPage, {
                    orderBy: 'updated_at',
                    order: 'desc',
                }, false);
                // Always close form when process ends
                toggleGIForm(false);
                // Auto-close notifications
                const timeout = hasNotificationErrors(notifications) ? NOTIF_ERROR_AUTOCLOSE_TIMEOUT : NOTIF_AUTOCLOSE_TIMEOUT;
                autoCloseNotifications(timeout);
            },
            onProgressUpdate: async () => {
                await fetchDSItems(1, datasetsPagination.perPage, {
                    orderBy: 'updated_at',
                    order: 'desc',
                }, false);
            },
            statusMessages: {
                complete: DATASET_PROCESSES_INFO['dataset_generating'].success,
                pause: sprintf(__("%s process has been paused.", 'limb-chatbot'), DATASET_PROCESSES_INFO['dataset_generating'].processName),
                cancel: sprintf(__("%s process has been cancelled.", 'limb-chatbot'), DATASET_PROCESSES_INFO['dataset_generating'].processName)
            },
            resetProgress: () => {
                setGeneratingProgress(false);
            },
        });
    }


    /**
     * Save index
     */
    const save = async () => {
        // Handle resume for paused processes
        if (processState && processState.status === 'pause') {
            if (processState.type === 'dataset_generating') {
                // Resume the paused process
                await resumeProcess();
            } else {
                handleError({
                    message: "Unknown process requested to resume.",
                    data: processState
                }, notifications.set, {
                    title: __("Unknown process requested to resume.", 'limb-chatbot'),
                    description: __("Please check and try again.", 'limb-chatbot'),
                });
            }
            return;
        }

        // Get form data
        const dataToSave = await giFormRef.current?.getDataToSave();
        if (!dataToSave) {
            // Errors are already shown
            return;
        }

        setSaving(true);
        try {
            // Convert form data to job config
            const generatingJobConfig = await convertFormDataToJobConfig(dataToSave.data);

            // Create generating job
            const generatingJob = await createJob({
                type: 'dataset_generating',
                sub_type: 'actionable',
                config: generatingJobConfig,
                chatbot_uuid: 'default'
            });

            // Don't set saving to false here - keep it true until the process completes
            // setSaving(false) will be called in onComplete callbacks
            setProcessState(null);

            // Follow the generating job
            await followDatasetGeneratingBackgroundProcess(generatingJob.id);
        } catch (e) {
            setSaving(false);
            handleError(e, notifications.set, {
                title: __("Failed to create job.", 'limb-chatbot'),
                description: e.message ? __(e.message, 'limb-chatbot') : __("Please check and try again.", 'limb-chatbot'),
            });
        }
    }

    const tableStructure = {
        columns: [
            {
                id: 'check',
                label: false,
                className: 'lbaic-settings-table-card-checkbox lbaic-settings-table-card-header-checkbox',
                render: () => {
                    const isAllState = selectedDSItems.length === 1 && selectedDSItems[0] === "all";
                    return <Checkbox
                        isChecked={isAllSelected || isAllState}
                        isIntermediateChecked={isPartiallySelected && !isAllState}
                        toggleValue={toggleAll}/>
                },
                value: {
                    className: 'lbaic-settings-table-card-checkbox lbaic-settings-table-card-body-checkbox',
                    render: (row, index) => {
                        const isChecked = selectedDSItems.indexOf(row.id) !== -1 || (selectedDSItems.length === 1 && selectedDSItems[0] === "all");
                        return <Checkbox
                            isChecked={isChecked}
                            toggleValue={() => {
                                // If "all" is selected, convert to actual IDs first
                                let currentSelected = selectedDSItems;
                                if (selectedDSItems.length === 1 && selectedDSItems[0] === "all") {
                                    currentSelected = [...datasets.map(item => item.id)];
                                }

                                const i = currentSelected.indexOf(row.id);
                                if (i === -1) {
                                    setSelectedDSItems([...currentSelected, row.id]);
                                } else {
                                    setSelectedDSItems(currentSelected.filter(item => item !== row.id));
                                }
                            }}/>
                    }
                },
            },
            {
                id: 'name',
                label: __("Name", 'limb-chatbot'),
                sortable: true,
                className: 'lbaic-settings-table-card-header-input',
                render: null,
                value: {
                    className: () => 'lbaic-settings-table-card-body-input',
                    render: (row, index) => renderName(row, index, {
                        preparing,
                        processState
                    }),
                },
            },
            {
                id: 'source',
                label: __("Source", 'limb-chatbot'),
                className: 'lbaic-settings-table-card-header-type',
                render: null,
                value: {
                    className: 'lbaic-settings-table-card-body-type',
                    render: (row, index) => renderSource(row),
                },
            },
            {
                id: 'ai',
                label: __("AI", 'limb-chatbot'),
                className: 'lbaic-settings-table-card-header-ai',
                render: null,
                value: {
                    className: 'lbaic-settings-table-card-body-ai',
                    render: (row, index) => renderAIProvider(row, kbAIProviderId, kbDimension),
                },
            },
            {
                id: 'storage',
                label: __("Storage", 'limb-chatbot'),
                className: 'lbaic-settings-table-card-header-storage',
                render: null,
                value: {
                    className: 'lbaic-settings-table-card-body-storage',
                    render: (row, index) => renderStorage(row)
                },
            },
            {
                id: 'updated_at',
                label: __("Last updated", 'limb-chatbot'),
                sortable: true,
                className: 'lbaic-settings-table-card-header-type',
                render: null,
                value: {
                    className: 'lbaic-settings-table-card-body-last-updated',
                    render: (row, index) => {
                        return <Chip>
                            <RealTimeRelativeTime
                                dateString={row.updated_at}
                                className='lbaic-settings-chip-label'
                            />
                        </Chip>
                    },
                },
            },
            {
                id: 'actions',
                label: false,
                className: 'lbaic-settings-table-card-header-actions',
                render: null,
                value: {
                    className: 'lbaic-settings-table-card-floating lbaic-settings-table-card-body-actions',
                    render: (row, index) => {
                        const rowDisabled = isRowDisabled(row, index, processState);

                        return <DatasetRowActions
                            row={row}
                            index={index}
                            processState={processState}
                            syncing={syncing}
                            deleting={deleting}
                            preparing={preparing}
                            onSync={sync}
                            onDelete={deleteDSItem}
                            isRowDisabled={rowDisabled}
                        />
                    }
                },
            },
        ],
        row: {
            className: (row, index) => {
                let classes = 'lbaic-settings-table-accordion-body-in';

                // Errors
                const errors = row.metas?.length ? row.metas.filter(item => item.meta_key === 'error').map(item => item.meta_value) : [];
                if (errors.length) {
                    classes += ' error';
                }
                return classes;
            },
            disabled: (row, index) => isRowDisabled(row, index, processState)
        },
        disableRowColumns: ['name', 'source', 'ai', 'storage', 'updated_at']
    };

    return <>
        <ContentBodyInner>
            {(showLoading || (loading > 0 && !isDataFetched)) ? <ContentLoading/> : null}
            <Container className="lbaic-settings-a-ei">
                    {addingEditing &&
                        <AddEdit ref={giFormRef}
                                 formData={formData}
                                 saving={Boolean(processState?.type)}
                                 setLoading={setLoading}
                                 setShowLoading={setShowLoading}
                                 updateHasUnsavedChanges={setHasUnsavedChanges}
                                 notifications={notifications}
                        />}
                    {
                        datasets.length > 0 ? (
                            <Table
                                className='lbaic-settings-a-es-table lbaic-settings-scroll-style lbaic-settings-scroll-x lbaic-settings-table-card-has-details lbaic-settings-table-card-kb-recommendations-items'
                                structure={tableStructure}
                                data={datasets}
                                loading={loading > 0 || !!processState?.type}
                                pagination={datasetsPagination}
                                order={{get: datasetsOrder, set: setDatasetsOrder}}
                                _callback={fetchDSItems}>
                            </Table>
                        ) : (
                            isDataFetched && !addingEditing && !datasetsPagination.total && (
                                <Empty title={__("No recommendations yet", 'limb-chatbot')}
                                       subtitle={__("Start by adding your first one", 'limb-chatbot')}
                                       icon="tab-indexes-active">
                                    <div className='lbaic-settings-empty-actions'>
                                        <button onClick={showAddDatasetsForm}
                                                disabled={!!processState?.type || preparing}
                                                className='lbaic-settings-button lbaic-settings-button-center lbaic-settings-button-h-40 lbaic-settings-button-primary'>
                                            <span
                                                className='lbaic-settings-button-label'>{__("Add recommendation", 'limb-chatbot')}</span>
                                        </button>
                                    </div>
                                </Empty>
                            )
                        )
                    }
                </Container>
        </ContentBodyInner>
        <Footer add={showAddDatasetsForm}
                save={save}
                resume={resumeProcess}
                pause={pauseProcess}
                cancel={cancelProcess}
                cancelForm={cancelForm}
                deleteSelected={deleteSelected}
                syncSelected={syncSelected}
                addingEditing={addingEditing}
                processState={processState}
                generatingProgress={generatingProgress}
                syncingProgress={syncingProgress}
                deletingProgress={deletingProgress}
                totalCount={datasetsPagination.total}
                loading={loading} saving={saving} syncing={syncing}
                deleting={deleting} pausing={pausing} canceling={canceling} preparing={preparing}
                selectedItems={selectedDSItems}
                isAllPageSelected={isAllPageSelected}
                onToggleSelectAll={toggleSelectAll}
                currentPageItemsCount={currentPageItemsCount}/>
    </>
}
