import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useCallback, useEffect, useMemo, useState } from 'react';

import { Button } from '../common/Button';
import { DownloadDialog } from './DownloadDialog/DownloadDialog.tsx';
import {
    DownloadSubmittedDataButton,
    MAX_SUBMITTED_DATA_DOWNLOAD_ENTRIES,
} from './DownloadDialog/DownloadSubmittedDataButton.tsx';
import { DownloadUrlGenerator } from './DownloadDialog/DownloadUrlGenerator.ts';
import { LinkOutMenu } from './DownloadDialog/LinkOutMenu.tsx';
import { FieldFilterSet, SequenceEntrySelection, type SequenceFilter } from './DownloadDialog/SequenceFilters.tsx';
import { RecentSequencesBanner } from './RecentSequencesBanner.tsx';
import { SearchForm } from './SearchForm';
import { SearchPagination } from './SearchPagination';
import { SeqPreviewModal } from './SeqPreviewModal';
import { Table, type TableSequenceData } from './Table';
import { TableColumnSelectorModal } from './TableColumnSelectorModal.tsx';
import { useSearchPageState } from './useSearchPageState.ts';
import { type QueryState } from './useStateSyncedWithUrlQueryParams.ts';
import { getLapisUrl } from '../../config.ts';
import { fetchDetailsFromLapis } from '../../services/lapisClientSideApi.ts';
import { lapisClientHooks } from '../../services/serviceHooks.ts';
import { DATA_USE_TERMS_FIELD, pageSize } from '../../settings';
import type { Group } from '../../types/backend.ts';
import type { LinkOut, ContactConfig } from '../../types/config.ts';
import { type FieldValues, type Schema, type SequenceFlaggingConfig } from '../../types/config.ts';
import { type OrderBy } from '../../types/lapis.ts';
import type { ReferenceGenomesInfo } from '../../types/referencesGenomes.ts';
import type { ClientConfig } from '../../types/runtimeConfig.ts';
import { formatNumberWithDefaultLocale } from '../../utils/formatNumber.tsx';
import {
    getColumnVisibilitiesFromQuery,
    getFieldVisibilitiesFromQuery,
    MetadataFilterSchema,
} from '../../utils/search.ts';
import { getSegmentAndGeneInfo } from '../../utils/sequenceTypeHelpers.ts';
import { EditDataUseTermsModal } from '../DataUseTerms/EditDataUseTermsModal.tsx';
import { ActiveFilters } from '../common/ActiveFilters.tsx';
import ErrorBox from '../common/ErrorBox.tsx';
import ErrorContactMessage from '../common/ErrorContactMessage.tsx';
import { Spinner } from '../common/Spinner';

export interface InnerSearchFullUIProps {
    accessToken?: string;
    referenceGenomesInfo: ReferenceGenomesInfo;
    myGroups: Group[];
    organism: string;
    clientConfig: ClientConfig;
    schema: Schema;
    hiddenFieldValues?: FieldValues;
    initialData: TableSequenceData[];
    initialCount: number;
    initialQueryDict: QueryState;
    showEditDataUseTermsControls?: boolean;
    dataUseTermsEnabled?: boolean;
    dataUseTermsAgreementHTML?: string;
    sequenceFlaggingConfig?: SequenceFlaggingConfig;
    linkOuts?: LinkOut[];
    contactConfig?: ContactConfig;
    groupId?: number;
    isReleasedPage: boolean;
}

const buildSequenceCountText = (totalSequences: number | undefined, oldCount: number | null, initialCount: number) => {
    const sequenceCount = totalSequences ?? oldCount ?? initialCount;

    const formattedCount = formatNumberWithDefaultLocale(sequenceCount);
    const pluralSuffix = sequenceCount === 1 ? '' : 's';

    return `Search returned ${formattedCount} sequence${pluralSuffix}`;
};

/* eslint-disable @typescript-eslint/no-unsafe-member-access -- TODO(#3451) this component is a mess a needs to be refactored */
const InnerSearchFullUI = ({
    accessToken,
    referenceGenomesInfo,
    myGroups,
    organism,
    clientConfig,
    schema,
    hiddenFieldValues,
    initialData,
    initialCount,
    initialQueryDict,
    showEditDataUseTermsControls = false,
    dataUseTermsEnabled = true,
    dataUseTermsAgreementHTML,
    sequenceFlaggingConfig,
    linkOuts,
    contactConfig,
    groupId,
    isReleasedPage,
}: InnerSearchFullUIProps) => {
    hiddenFieldValues ??= {};

    const metadataSchema = schema.metadata;
    const multiFieldSearches = schema.multiFieldSearches;
    const filterSchema = useMemo(
        () => new MetadataFilterSchema(metadataSchema, multiFieldSearches),
        [metadataSchema, multiFieldSearches],
    );

    const [isColumnModalOpen, setIsColumnModalOpen] = useState(false);

    const {
        state,
        previewedSeqId,
        setPreviewedSeqId,
        previewHalfScreen,
        setPreviewHalfScreen,
        referenceSelection,
        page,
        setPage,
        setSomeFieldValues,
        removeFilter,
        orderByField: orderByFieldCandidate,
        orderDirection,
        setOrderByField,
        setOrderDirection,
        setASearchVisibility,
        setAColumnVisibility,
    } = useSearchPageState({ initialQueryDict, schema, hiddenFieldValues, filterSchema, referenceGenomesInfo });

    const searchVisibilities = useMemo(() => {
        return getFieldVisibilitiesFromQuery(schema, state);
    }, [schema, state]);

    const columnVisibilities = useMemo(() => getColumnVisibilitiesFromQuery(schema, state), [schema, state]);

    const columnsToShow = useMemo(() => {
        return schema.metadata
            .filter(
                (field) =>
                    columnVisibilities
                        .get(field.name)
                        ?.isVisible(referenceGenomesInfo, referenceSelection?.selectedReferences) === true,
            )
            .map((field) => field.name);
    }, [schema.metadata, columnVisibilities, referenceSelection?.selectedReferences, referenceGenomesInfo]);

    const orderByField = columnsToShow.includes(orderByFieldCandidate) ? orderByFieldCandidate : schema.primaryKey;

    /**
     * The `fieldValues` are the values of the search fields.
     * The values are initially loaded from the default values set in `hiddenFieldValues`
     * and the initial `state` (URL search params).
     */
    const fieldValues = useMemo(() => {
        return filterSchema.getFieldValuesFromQuery(state, hiddenFieldValues, referenceGenomesInfo);
    }, [state, hiddenFieldValues, filterSchema, referenceGenomesInfo]);

    useEffect(() => {
        if (showEditDataUseTermsControls && dataUseTermsEnabled) {
            setAColumnVisibility(DATA_USE_TERMS_FIELD, true);
        }
    }, []);

    const lapisUrl = getLapisUrl(clientConfig, organism);
    const downloadUrlGenerator = new DownloadUrlGenerator(
        organism,
        lapisUrl,
        dataUseTermsEnabled,
        schema.richFastaHeaderFields,
    );

    const hooks = lapisClientHooks(lapisUrl);
    const aggregatedHook = hooks.useAggregated();
    const detailsHook = hooks.useDetails();

    const [selectedSeqs, setSelectedSeqs] = useState(new Set<string>());
    const sequencesSelected = selectedSeqs.size > 0;
    const clearSelectedSeqs = () => setSelectedSeqs(new Set());

    const segmentAndGeneInfo = useMemo(
        () => getSegmentAndGeneInfo(referenceGenomesInfo, referenceSelection?.selectedReferences),
        [referenceGenomesInfo, referenceSelection?.selectedReferences],
    );

    const tableFilter = useMemo(
        () =>
            new FieldFilterSet(filterSchema, fieldValues, hiddenFieldValues, segmentAndGeneInfo, referenceGenomesInfo),
        [fieldValues, hiddenFieldValues, referenceGenomesInfo, filterSchema],
    );

    /**
     * The `lapisSearchParameters` are derived from the `fieldValues` (the search boxes).
     * Some values are modified slightly or expanded based on field definitions.
     */
    const lapisSearchParameters = useMemo(() => tableFilter.toApiParams(), [tableFilter]);

    const downloadFilter: SequenceFilter = sequencesSelected ? new SequenceEntrySelection(selectedSeqs) : tableFilter;

    useEffect(() => {
        aggregatedHook.mutate({
            ...lapisSearchParameters,
            fields: [],
        });
        const OrderByList: OrderBy[] = [
            {
                field: orderByField,
                type: orderDirection,
            },
        ];
        // @ts-expect-error because the hooks don't accept OrderBy
        detailsHook.mutate({
            ...lapisSearchParameters,
            fields: [...columnsToShow, schema.primaryKey],
            limit: pageSize,
            offset: (page - 1) * pageSize,
            orderBy: OrderByList,
        });
    }, [lapisSearchParameters, schema.tableColumns, schema.primaryKey, pageSize, page, orderByField, orderDirection]);

    const totalSequences = aggregatedHook.data?.data[0].count ?? undefined;
    const linkOutSequenceCount = downloadFilter.sequenceCount() ?? totalSequences;

    const fetchAccessions = useCallback(async (): Promise<string[]> => {
        const response = await fetchDetailsFromLapis(lapisUrl, {
            ...lapisSearchParameters,
            fields: [schema.primaryKey],
            limit: MAX_SUBMITTED_DATA_DOWNLOAD_ENTRIES + 1,
        });
        return response.data.map((item) => String(item[schema.primaryKey]));
    }, [lapisUrl, lapisSearchParameters, schema.primaryKey]);

    const [oldData, setOldData] = useState<TableSequenceData[] | null>(null);
    const [oldCount, setOldCount] = useState<number | null>(null);
    const [firstClientSideLoadOfDataCompleted, setFirstClientSideLoadOfDataCompleted] = useState(false);
    const [firstClientSideLoadOfCountCompleted, setFirstClientSideLoadOfCountCompleted] = useState(false);

    useEffect(() => {
        if (detailsHook.data?.data && oldData !== detailsHook.data.data) {
            setOldData(detailsHook.data.data);
            setFirstClientSideLoadOfDataCompleted(true);
        }
    }, [detailsHook.data?.data, oldData]);

    useEffect(() => {
        if (aggregatedHook.data?.data && oldCount !== aggregatedHook.data.data[0].count) {
            setOldCount(aggregatedHook.data.data[0].count);
            setFirstClientSideLoadOfCountCompleted(true);
        }
    }, [aggregatedHook.data?.data, oldCount]);

    const showMutationSearch = schema.submissionDataTypes.consensusSequences;

    return (
        <div className='flex flex-col md:flex-row gap-8 md:gap-4'>
            <TableColumnSelectorModal
                isOpen={isColumnModalOpen}
                onClose={() => setIsColumnModalOpen(!isColumnModalOpen)}
                schema={schema}
                columnVisibilities={columnVisibilities}
                setAColumnVisibility={setAColumnVisibility}
                selectedReferenceNames={referenceSelection?.selectedReferences}
                referenceGenomesInfo={referenceGenomesInfo}
            />
            <SeqPreviewModal
                key={previewedSeqId ?? 'seq-modal'}
                seqId={previewedSeqId ?? ''}
                accessToken={accessToken}
                isOpen={Boolean(previewedSeqId)}
                onClose={() => setPreviewedSeqId(null)}
                referenceGenomesInfo={referenceGenomesInfo}
                myGroups={myGroups}
                isHalfScreen={previewHalfScreen}
                setIsHalfScreen={setPreviewHalfScreen}
                setPreviewedSeqId={(seqId: string | null) => setPreviewedSeqId(seqId)}
                sequenceFlaggingConfig={sequenceFlaggingConfig}
            />
            <div className='md:w-[18rem]'>
                <SearchForm
                    organism={organism}
                    clientConfig={clientConfig}
                    referenceGenomesInfo={referenceGenomesInfo}
                    fieldValues={fieldValues}
                    setSomeFieldValues={setSomeFieldValues}
                    filterSchema={filterSchema}
                    lapisUrl={lapisUrl}
                    searchVisibilities={searchVisibilities}
                    setASearchVisibility={setASearchVisibility}
                    lapisSearchParameters={lapisSearchParameters}
                    showMutationSearch={showMutationSearch}
                    referenceSelection={referenceSelection}
                />
            </div>
            <div
                className='flex-1 min-w-0'
                style={{ paddingBottom: Boolean(previewedSeqId) && previewHalfScreen ? '50vh' : '0' }}
            >
                <RecentSequencesBanner organism={organism} />

                {(detailsHook.isError || aggregatedHook.isError) &&
                    // @ts-expect-error because response is not expected on error, but does exist
                    (aggregatedHook.error?.response?.status === 503 ? (
                        <div className='p-3 rounded-lg text-lg text-gray-700 text-italic'>
                            {' '}
                            The retrieval database is currently initializing – please check back later.
                        </div>
                    ) : (
                        <div className='bg-red-400 p-3 rounded-lg'>
                            <p>There was an error loading the data</p>
                            <details>
                                <summary className='text-xs cursor-pointer py-2'>More details</summary>
                                <p className='text-xs'>{JSON.stringify(detailsHook.error)}</p>

                                <p>{detailsHook.error?.message}</p>
                                <p>{aggregatedHook.error?.message}</p>
                            </details>
                        </div>
                    ))}
                {(detailsHook.isPaused || aggregatedHook.isPaused) &&
                    (!detailsHook.isSuccess || !aggregatedHook.isSuccess) && (
                        <ErrorBox title='Connection problem'>
                            <div className='space-y-2 mt-2'>
                                <p>
                                    The browser thinks you are offline. This will affect site usage, and many features
                                    may not work. If you are actually online, please try using a different browser.
                                </p>
                                <ErrorContactMessage {...contactConfig} />
                            </div>
                        </ErrorBox>
                    )}

                <div
                    className={`
                        ${
                            !(firstClientSideLoadOfCountCompleted && firstClientSideLoadOfDataCompleted)
                                ? 'cursor-wait pointer-events-none'
                                : detailsHook.isPending || aggregatedHook.isPending
                                  ? 'opacity-50 pointer-events-none'
                                  : ''
                        }
                        `}
                >
                    {!tableFilter.isEmpty() && (
                        <div className='pt-3 pb-2'>
                            <ActiveFilters sequenceFilter={tableFilter} removeFilter={removeFilter} />
                        </div>
                    )}
                    <div className='text-sm text-gray-800 mb-6 justify-between flex flex-wrap gap-4'>
                        <div className='mt-auto'>
                            {buildSequenceCountText(totalSequences, oldCount, initialCount)}
                            {detailsHook.isPending ||
                            aggregatedHook.isPending ||
                            !firstClientSideLoadOfCountCompleted ||
                            !firstClientSideLoadOfDataCompleted ? (
                                <span className='ml-3 appearSlowly inline-block'>
                                    <Spinner size='xs' />
                                </span>
                            ) : null}
                        </div>
                        <div className='flex flex-wrap justify-start gap-2'>
                            {showEditDataUseTermsControls && dataUseTermsEnabled && (
                                <EditDataUseTermsModal
                                    lapisUrl={lapisUrl}
                                    clientConfig={clientConfig}
                                    accessToken={accessToken}
                                    sequenceFilter={downloadFilter}
                                />
                            )}
                            <Button
                                className='mr-2 underline text-primary-700 hover:text-primary-500'
                                onClick={() => setIsColumnModalOpen(true)}
                            >
                                Customize columns
                            </Button>
                            {sequencesSelected ? (
                                <Button
                                    className='mr-2 underline text-primary-700 hover:text-primary-500'
                                    onClick={clearSelectedSeqs}
                                >
                                    Clear selection
                                </Button>
                            ) : null}

                            <DownloadDialog
                                downloadUrlGenerator={downloadUrlGenerator}
                                sequenceFilter={downloadFilter}
                                referenceGenomesInfo={referenceGenomesInfo}
                                allowSubmissionOfConsensusSequences={schema.submissionDataTypes.consensusSequences}
                                dataUseTermsEnabled={dataUseTermsEnabled}
                                dataUseTermsAgreementHTML={dataUseTermsAgreementHTML}
                                schema={schema}
                                richFastaHeaderFields={schema.richFastaHeaderFields}
                                selectedReferenceNames={referenceSelection?.selectedReferences}
                                referenceIdentifierField={schema.referenceIdentifierField}
                            />
                            {isReleasedPage && accessToken !== undefined && groupId !== undefined && (
                                <DownloadSubmittedDataButton
                                    sequenceFilter={downloadFilter}
                                    backendUrl={clientConfig.backendUrl}
                                    accessToken={accessToken}
                                    organism={organism}
                                    groupId={groupId}
                                    totalSequences={totalSequences}
                                    fetchAccessions={fetchAccessions}
                                />
                            )}
                            {linkOuts !== undefined && linkOuts.length > 0 && (
                                <LinkOutMenu
                                    downloadUrlGenerator={downloadUrlGenerator}
                                    sequenceFilter={downloadFilter}
                                    sequenceCount={linkOutSequenceCount}
                                    linkOuts={linkOuts}
                                    dataUseTermsEnabled={dataUseTermsEnabled}
                                    referenceGenomesInfo={referenceGenomesInfo}
                                    referenceSelection={referenceSelection}
                                />
                            )}
                        </div>
                    </div>

                    <Table
                        schema={schema}
                        data={detailsHook.data?.data ?? oldData ?? initialData}
                        selectedSeqs={selectedSeqs}
                        setSelectedSeqs={setSelectedSeqs}
                        setPreviewedSeqId={(seqId: string | null) => setPreviewedSeqId(seqId)}
                        previewedSeqId={previewedSeqId}
                        orderBy={{
                            field: orderByField,
                            type: orderDirection,
                        }}
                        setOrderByField={setOrderByField}
                        setOrderDirection={setOrderDirection}
                        columnsToShow={columnsToShow}
                    />

                    <div className='mt-4 flex justify-center'>
                        {totalSequences !== undefined && (
                            <SearchPagination
                                count={Math.ceil(totalSequences / pageSize)}
                                page={page}
                                setPage={setPage}
                            />
                        )}
                    </div>
                </div>
            </div>
        </div>
    );
};

export const SearchFullUI = (props: InnerSearchFullUIProps) => {
    const queryClient = new QueryClient();

    return (
        <QueryClientProvider client={queryClient}>
            <InnerSearchFullUI {...props} />
        </QueryClientProvider>
    );
};
