import type { MouseEvent } from 'react';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import {
    Button,
    LoadingIndicator,
    HelpTooltip,
    SegmentedInput,
    Select,
} from '@theorchard/suite-components';
import {
    formatMessage,
    Page,
    Segment,
    useIdentity,
} from '@theorchard/suite-frontend';
import { GlyphIcon } from '@theorchard/suite-icons';
import { capitalize } from 'lodash';
import { useHistory, useLocation, Link } from 'react-router-dom';
import BrandSelector from 'src/components/brandSelector';
import ReadError from 'src/components/errors/read-error';
import HeaderLabelPicker from 'src/components/headerLabelPicker';
import { SHOW_ALL_LABEL_UUID } from 'src/components/headerLabelPicker/labelSearchDropdown';
import SongWriterMultiSelect from 'src/components/songWriterMultiSelector/songWriterMultiSelector';
import { FEATURE_FLAGS } from 'src/constants';
import { useDeleteComposition } from 'src/data/mutations';
import { useCompositions } from 'src/data/queries';
import { CompanyBrandName } from 'src/data/schemaTypes';
import SongTable from 'src/pages/songListPage/songTable';
import { useApplicationContext } from 'src/utils/applicationContext';
import { useDebounceCallback } from 'src/utils/debounce';
import { useIsEmployee, useIsEmployeeWithFullAccess } from 'src/utils/helpers';
import { useLocationSearchQuery } from 'src/utils/route';
import { songUrl, newSongUrl, songsUrlFromFilter } from 'src/utils/urls';
import type { BrandOption } from 'src/components/brandSelector';
import type { PublishingCompositionsListingFilter } from 'src/data/queries';
import type { Label } from 'src/types';

export const CLASSNAME = 'SongListPage';
export const SONGS_PER_PAGE = 25;
export const SEARCH_DEBOUNCE_INTERVAL = 500;

const category = 'Songs';

const enum URLParams {
    PAGE = 'page',
    LABEL_ID = 'labelId',
    TERM = 'term',
    SONG_WRITERS = 'songWriters',
    BRAND = 'brand',
    DELIVERED = 'delivered',
}

const enum DeliveredOption {
    ALL = 'all',
    DELIVERED = 'true',
    NOT_DELIVERED = 'false',
}

const DELIVERED_OPTIONS = [
    { label: formatMessage('generic.all'), value: DeliveredOption.ALL },
    {
        label: formatMessage('generic.delivered'),
        value: DeliveredOption.DELIVERED,
    },
    {
        label: formatMessage('generic.notDelivered'),
        value: DeliveredOption.NOT_DELIVERED,
    },
];

// `undefined` means "all", so it cannot be round tripped through the URL as a
// boolean: an absent param is the unfiltered state.
const parseDelivered = (value: string | null) => {
    if (value === 'true') return true;
    if (value === 'false') return false;
    return undefined;
};

const deliveredToOption = (delivered?: boolean | null) => {
    if (delivered === true) return DeliveredOption.DELIVERED;
    if (delivered === false) return DeliveredOption.NOT_DELIVERED;
    return DeliveredOption.ALL;
};
// Workaround for React 18 + react-router-dom v5 type incompatibility
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const LinkAny = Link as any;

const extractValuesFromSearchQuery = (
    searchQuery: URLSearchParams,
    isEmployeeWithFullAccess: boolean = false
) => {
    const page = parseInt(searchQuery.get(URLParams.PAGE) || '1', 10);
    const titleSearch = decodeURIComponent(
        searchQuery.get(URLParams.TERM) || ''
    );
    const labelId = searchQuery.get(URLParams.LABEL_ID)
        ? decodeURIComponent(searchQuery.get(URLParams.LABEL_ID)!)
        : null;
    const songWriters = searchQuery.get(URLParams.SONG_WRITERS)
        ? decodeURIComponent(searchQuery.get(URLParams.SONG_WRITERS)!)
        : null;

    const brandValue = (
        isEmployeeWithFullAccess && searchQuery.get(URLParams.BRAND)
            ? decodeURIComponent(searchQuery.get(URLParams.BRAND)!)
            : undefined
    ) as CompanyBrandName;

    const delivered = parseDelivered(searchQuery.get(URLParams.DELIVERED));

    return {
        page,
        titleSearch,
        delivered,
        labelId: parseInt(labelId ?? '0', 10),
        songWriters,
        brand: Object.values(CompanyBrandName).includes(brandValue)
            ? brandValue
            : undefined,
    };
};

const SongListPage: React.FC<object> = () => {
    const {
        label: contextLabel,
        pageFilters,
        updatePageFilters,
    } = useApplicationContext();

    const history = useHistory();
    const { pathname } = useLocation();
    const searchQuery = useLocationSearchQuery();
    const deleteComposition = useDeleteComposition();
    const identity = useIdentity();
    const isEmployee = useIsEmployee();
    const isEmployeeWithFullAccess = useIsEmployeeWithFullAccess();

    const adminUxImprovementsEnabled =
        !!identity.features[FEATURE_FLAGS.ADMIN_UX_IMPROVEMENTS];

    const tab = pathname.replace('/songs/', '');
    const {
        page,
        titleSearch: songTitleFromSearchQuery,
        delivered: deliveredFromSearchQuery,
        labelId,
        songWriters: songWritersFromSearchQuery,
        brand: brandFromSearchQuery,
    } = extractValuesFromSearchQuery(searchQuery, isEmployeeWithFullAccess);
    const isDraft = tab === 'drafts';

    // Behind the flag the delivery filter does not exist at all, so the URL
    // param is ignored and never reaches the query.
    const delivered = adminUxImprovementsEnabled
        ? deliveredFromSearchQuery
        : undefined;

    const brand =
        brandFromSearchQuery || (pageFilters?.brand as CompanyBrandName);
    const titleSearch = songTitleFromSearchQuery || pageFilters?.songPageTitle;
    const songWriters =
        songWritersFromSearchQuery || pageFilters?.songPageSongWriters;
    const label: Label | null = useMemo(
        () =>
            !contextLabel || contextLabel.uuid === SHOW_ALL_LABEL_UUID
                ? null
                : contextLabel,
        [contextLabel]
    );

    const [filter, setFilter] = useState<PublishingCompositionsListingFilter>({
        label,
        songWriters: songWriters?.split(','),
        page,
        titleSearch,
        draft: isDraft,
        brand,
        // The gated filters are spread in rather than set to `undefined` so
        // that, with the flag off, the query variables are exactly the ones
        // master sends.
        ...(adminUxImprovementsEnabled && {
            delivered,
            // Employees can also find a song by its pub song ID through the
            // search box. A non numeric term simply does not match any ID, so
            // the title search keeps working as before.
            titleAndSongIdSearch: isEmployee,
        }),
    });

    // The search input is uncoupled from the filter so typing stays responsive
    // while the query it triggers is debounced.
    const [searchTerm, setSearchTerm] = useState(titleSearch ?? '');

    const [pageSize, setPageSize] = useState(SONGS_PER_PAGE);
    const handleChangedPageSize = (newPageSize: number) => {
        setPageSize(newPageSize);
    };

    useEffect(() => {
        history.push(songsUrlFromFilter(tab, filter, isEmployeeWithFullAccess));
    }, [filter, tab, history, isEmployeeWithFullAccess]);

    useEffect(() => {
        void Segment.trackEvent(`Click - ${capitalize(tab)} Tab`, { category });

        setFilter(prevFilter => {
            const updateFilterState =
                prevFilter.draft !== isDraft ||
                prevFilter.page !== page ||
                prevFilter.titleSearch !== titleSearch ||
                prevFilter.delivered !== delivered ||
                (prevFilter.songWriters?.join(',') ?? null) !== songWriters ||
                prevFilter.label?.uuid !== label?.uuid ||
                prevFilter.brand !== brand;

            return updateFilterState
                ? {
                      ...prevFilter,
                      titleSearch,
                      songWriters: songWriters?.split(','),
                      label,
                      page,
                      draft: isDraft,
                      brand,
                      ...(adminUxImprovementsEnabled && { delivered }),
                  }
                : prevFilter;
        });
    }, [
        adminUxImprovementsEnabled,
        isDraft,
        label,
        page,
        tab,
        titleSearch,
        delivered,
        songWriters,
        brand,
    ]);

    // Keeps the input in step with a term that changed outside of it (e.g. back
    // / forward navigation). Typing does not move `titleSearch` until the
    // debounce fires, so this never clobbers what is being typed.
    useEffect(() => {
        setSearchTerm(titleSearch ?? '');
    }, [titleSearch]);

    const {
        data: songsData,
        loading: loadingSongs,
        error,
    } = useCompositions(filter, pageSize);

    const applySearchTerm = useCallback(
        (term: string) => {
            updatePageFilters({
                songPageTitle: term,
            });
            setFilter(prevFilter => ({
                ...prevFilter,
                titleSearch: term,
            }));
        },
        [updatePageFilters]
    );

    const debouncedApplySearchTerm = useDebounceCallback(
        applySearchTerm,
        SEARCH_DEBOUNCE_INTERVAL
    );

    const onChangeSearchTerm = (e: React.ChangeEvent<HTMLInputElement>) => {
        setSearchTerm(e.target.value);
        debouncedApplySearchTerm(e.target.value);
    };

    const onChangeDelivered = (option?: { value: string }) => {
        setFilter(prevFilter => ({
            ...prevFilter,
            delivered: parseDelivered(option?.value ?? null),
        }));
    };

    // PUB-1063
    // If the total count of songs changes, we need to check if the current page is still valid
    // if not, we update the page number to the first page
    const getNewPageNumber = useCallback(() => {
        const numPages = Math.ceil((songsData.totalCount ?? 0) / pageSize);
        const newPageNumber = numPages === 0 || page > numPages ? 1 : page;
        return newPageNumber;
    }, [songsData, pageSize, page]);

    useEffect(() => {
        if (!loadingSongs && songsData?.totalCount !== undefined) {
            const newPageNumber = getNewPageNumber();
            if (newPageNumber !== page) {
                setFilter(prevFilter => ({
                    ...prevFilter,
                    page: newPageNumber,
                }));
            }
        }
    }, [loadingSongs, songsData, getNewPageNumber, page]);

    const onToggleDropdown = (dropdownOpen: boolean) => {
        if (dropdownOpen) void Segment.trackEvent('Click - ...', { category });
    };

    const onChangedSongWriters = useCallback(
        (songWriters: string[] | null) => {
            updatePageFilters({
                songPageSongWriters: songWriters?.join(',') || null,
            });
            setFilter(f => ({
                ...f,
                songWriters,
            }));
        },
        [updatePageFilters]
    );

    const onChangeBrand = (option?: BrandOption) => {
        updatePageFilters({ brand: option?.value || null });
        setFilter(prevFilter => ({
            ...prevFilter,
            brand: option?.value as CompanyBrandName,
        }));
    };

    const onDelete = (id: string) => {
        const song = songsData.compositions?.find(c => c.id === id);
        void Segment.trackEvent('Click - Delete Song', {
            category: 'Delete Song',
            songId: id,
            songTitle: song?.title,
        });
        void deleteComposition({ variables: { id } });
    };

    const onEdit = (id: string) => {
        void Segment.trackEvent('Click - Edit Song', { category });
        history.push(songUrl(id));
    };

    const onClickSong = () => {
        void Segment.trackEvent('Click - Edit Song', { category });
    };

    const onPaginationChange = (newPage: number) => {
        setFilter({
            ...filter,
            page: newPage + 1,
        });
    };

    const isNewSongDisabled = !label;
    const onNewSongClick = (event: MouseEvent) => {
        if (isNewSongDisabled) event.preventDefault();
    };
    const newSongButton = (
        <Button
            disabled={isNewSongDisabled}
            className="icon-left"
            variant="primary"
            size="lg"
            onClick={() => {
                void Segment.trackEvent('Click - New Song', { category });
            }}
        >
            <GlyphIcon name="plus" size={12} />
            <span>{formatMessage('generic.newSong')}</span>
        </Button>
    );

    if (!songsData) return <LoadingIndicator />;

    if (error) return <ReadError error={error} />;

    return (
        <div className={CLASSNAME}>
            <Page.Header>
                <HeaderLabelPicker />
                <Page.Nav
                    route="/songs/:tab"
                    params={{
                        page,
                        labelId,
                        term: titleSearch,
                        ...(delivered === undefined
                            ? {}
                            : { delivered: String(delivered) }),
                        songWriters,
                        brand,
                    }}
                    items={[
                        {
                            key: 'submitted',
                            label: 'generic.submitted',
                        },
                        {
                            key: 'drafts',
                            label: 'generic.drafts',
                        },
                    ]}
                    right={
                        <LinkAny
                            id="NewSongButton"
                            data-testid="NewSongButton"
                            className="mb-3"
                            onClick={onNewSongClick}
                            to={newSongUrl()}
                        >
                            {isNewSongDisabled ? (
                                <HelpTooltip
                                    message={formatMessage(
                                        'newSong.newSongDisabled'
                                    )}
                                    tooltipPlacement="left"
                                    id="new-song-tooltip"
                                >
                                    {newSongButton}
                                </HelpTooltip>
                            ) : (
                                newSongButton
                            )}
                        </LinkAny>
                    }
                />
            </Page.Header>

            <Page.Body key={`${tab}-${page}-body`}>
                <Page.Toolbar className="d-flex flex-row my-4">
                    <SegmentedInput className="col-4 pl-0">
                        <SegmentedInput.Static>
                            <GlyphIcon name="search" size={16} />
                        </SegmentedInput.Static>

                        <input
                            type="text"
                            className="form-control"
                            value={searchTerm}
                            placeholder={formatMessage('generic.search')}
                            onChange={onChangeSearchTerm}
                        />
                    </SegmentedInput>

                    <div className="col-4">
                        <SongWriterMultiSelect
                            label={filter?.label || null}
                            onChange={onChangedSongWriters}
                            defaultValue={filter?.songWriters?.map(i => ({
                                legalName: i ? i : '',
                            }))}
                        />
                    </div>

                    {adminUxImprovementsEnabled && (
                        <div className="col-2">
                            <Select
                                className="DeliveredSelector"
                                placeholder={formatMessage(
                                    'generic.deliveryStatus'
                                )}
                                onChange={onChangeDelivered}
                                initialValue={deliveredToOption(
                                    filter.delivered
                                )}
                                options={DELIVERED_OPTIONS}
                                hideFilter
                                hideClearButton
                                menuWidth="100%"
                                menuMinWidth="100%"
                            />
                        </div>
                    )}

                    {isEmployeeWithFullAccess && (
                        <div className="col-2">
                            <BrandSelector
                                initialValue={brand}
                                onChange={onChangeBrand}
                            />
                        </div>
                    )}
                </Page.Toolbar>

                <Page.View
                    loading={false} // We need to fake loading/hasData as we use custom "EmptyState"
                    hasData
                    error={error}
                >
                    <SongTable
                        isDraft={isDraft}
                        isLoading={loadingSongs}
                        songsData={songsData}
                        page={page - 1}
                        pageSize={pageSize}
                        showPubSongId={isEmployee}
                        onClickSong={onClickSong}
                        onEditSong={onEdit}
                        onDeleteSong={onDelete}
                        onToggleSongRowDropDown={onToggleDropdown}
                        onPaginationChange={onPaginationChange}
                        onSetPageSize={handleChangedPageSize}
                    />
                </Page.View>
            </Page.Body>
        </div>
    );
};

export default SongListPage;
