import {
    AddLinkEventTracker,
    ApproveAllMembershipRequestsMessageComposer,
    GetSessionDataManager,
    GroupAdminGiveComposer,
    GroupAdminTakeComposer,
    GroupConfirmMemberRemoveEvent,
    GroupMemberParser,
    GroupMembersComposer,
    GroupMembersEvent,
    GroupMembershipAcceptComposer,
    GroupMembershipDeclineComposer,
    GroupMembersParser,
    GroupMembersRefreshEvent,
    GroupMemberUpdateEvent,
    GroupRank,
    GroupRemoveMemberComposer,
    HabboGroupDeactivatedMessageEvent,
    ILinkEventTracker,
    RemoveLinkEventTracker,
    UnblockGroupMemberMessageComposer
} from '@octane/renderer';
import { FC, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import { FaBan, FaChevronLeft, FaChevronRight, FaUndo } from 'react-icons/fa';
import { GetConfigurationValue, GetUserProfile, LocalizeText, localizeWithFallback, SendMessageComposer, TrailingThrottle } from '../../../api';
import {
    Button,
    Column,
    Flex,
    Grid,
    LayoutAvatarImageView,
    LayoutBadgeImageView,
    OctaneCardContentView,
    OctaneCardHeaderView,
    OctaneCardView,
    Text
} from '../../../common';
import { useGroupMemberRemoval, useMessageEvent, useNotification } from '../../../hooks';
import { classNames } from '../../../layout';
import { Select } from '../../../common/Select';
import { NumberInput } from '../../../common/NumberInput';
import { BaseButton } from '../../../common/BaseButton';
import { Hint } from '../../../common/Tooltip';
import { NativeTextTree } from '../../../common/native-text/NativeTextTree';

const MEMBERS_REQUEST_INTERVAL_MS = 550;

export const GroupMembersView: FC<{}> = (props) => {
    const [groupId, setGroupId] = useState<number>(-1);
    const [levelId, setLevelId] = useState<number>(-1);
    const [membersData, setMembersData] = useState<GroupMembersParser>(null);
    const [pageId, setPageId] = useState<number>(-1);
    const [totalPages, setTotalPages] = useState<number>(0);
    const [searchQuery, setSearchQuery] = useState<string>('');
    const { showConfirm = null } = useNotification();
    const pendingActionsRef = useRef<Set<string>>(new Set());
    // The confirm reply only carries the user id, so remember who it was for and whether it was a kick or a block.
    const pendingRemovalRef = useRef<{ groupId: number; userId: number; name: string; block: boolean }>(null);
    // One outstanding GroupConfirmRemoveMember for every group window (Leave included), see useGroupMemberRemoval.
    const { request: requestMemberRemoval, claimReply, isCurrentSession } = useGroupMemberRemoval();
    const groupIdRef = useRef<number>(-1);
    const blockingEnabled = GetConfigurationValue<boolean>('group.blocking.enabled', true);

    // Read by the removal confirmation callback; refreshed before it can run.
    useLayoutEffect(() => {
        groupIdRef.current = groupId;
    });

    const askRemoval = (member: GroupMemberParser, block: boolean) => {
        if (requestMemberRemoval(membersData.groupId, member.id)) pendingRemovalRef.current = { groupId: membersData.groupId, userId: member.id, name: member.name, block };
    };

    const getRankDescription = (member: GroupMemberParser) => {
        if (member.rank === GroupRank.OWNER) return 'group.members.owner';

        if (membersData.admin) {
            if (member.rank === GroupRank.ADMIN) return 'group.members.removerights';

            if (member.rank === GroupRank.MEMBER) return 'group.members.giverights';
        }

        return '';
    };

    // The server answers one members request per 500 ms and drops the rest, and a reply that is not for the current
    // query and page is ignored: typing "abc" fast got only the reply for "a", and the list stayed unfiltered.
    // The latest request is sent once the interval has passed.
    const [membersRequests] = useState(() => new TrailingThrottle(MEMBERS_REQUEST_INTERVAL_MS));

    const refreshMembers = useCallback(() => {
        if (groupId === -1 || levelId === -1 || pageId === -1) return;

        membersRequests.run(() => SendMessageComposer(new GroupMembersComposer(groupId, pageId, searchQuery, levelId)));
    }, [groupId, levelId, pageId, searchQuery, membersRequests]);

    const toggleAdmin = (member: GroupMemberParser) => {
        if (!membersData.admin || member.rank === GroupRank.OWNER) return;

        const key = `admin_${member.id}`;
        if (pendingActionsRef.current.has(key)) return;
        pendingActionsRef.current.add(key);
        setTimeout(() => pendingActionsRef.current.delete(key), 2000);

        if (member.rank !== GroupRank.ADMIN) SendMessageComposer(new GroupAdminGiveComposer(membersData.groupId, member.id));
        else SendMessageComposer(new GroupAdminTakeComposer(membersData.groupId, member.id));
    };

    const acceptMembership = (member: GroupMemberParser) => {
        if (!membersData.admin || member.rank !== GroupRank.REQUESTED) return;

        const key = `accept_${member.id}`;
        if (pendingActionsRef.current.has(key)) return;
        pendingActionsRef.current.add(key);
        setTimeout(() => pendingActionsRef.current.delete(key), 2000);

        SendMessageComposer(new GroupMembershipAcceptComposer(membersData.groupId, member.id));
    };

    const runOnce = (key: string) => {
        if (pendingActionsRef.current.has(key)) return false;
        pendingActionsRef.current.add(key);
        setTimeout(() => pendingActionsRef.current.delete(key), 2000);

        return true;
    };

    const blockMember = (member: GroupMemberParser) => {
        if (!membersData.admin || !blockingEnabled) return;
        if (member.rank !== GroupRank.MEMBER && member.rank !== GroupRank.ADMIN) return;
        if (!runOnce(`remove_${member.id}`)) return;

        askRemoval(member, true);
    };

    const unblockMember = (member: GroupMemberParser) => {
        if (!membersData.admin || member.rank !== GroupRank.BLOCKED) return;
        if (!runOnce(`unblock_${member.id}`)) return;

        SendMessageComposer(new UnblockGroupMemberMessageComposer(membersData.groupId, member.id));
    };

    const acceptAllMemberships = () => {
        if (!membersData.admin || !runOnce('accept_all')) return;

        SendMessageComposer(new ApproveAllMembershipRequestsMessageComposer(membersData.groupId));
    };

    const removeMemberOrDeclineMembership = (member: GroupMemberParser) => {
        if (!membersData.admin) return;

        const key = `remove_${member.id}`;
        if (pendingActionsRef.current.has(key)) return;
        pendingActionsRef.current.add(key);
        setTimeout(() => pendingActionsRef.current.delete(key), 2000);

        if (member.rank === GroupRank.REQUESTED) {
            SendMessageComposer(new GroupMembershipDeclineComposer(membersData.groupId, member.id));

            return;
        }

        askRemoval(member, false);
    };

    useMessageEvent<GroupMembersEvent>(GroupMembersEvent, (event) => {
        const parser = event.getParser();
        const normalizedLevel = !parser.admin && levelId >= 2 && parser.level === 0;

        if (parser.groupId !== groupId || parser.query !== searchQuery || parser.pageIndex !== pageId) return;
        if (parser.level !== levelId && !normalizedLevel) return;

        setMembersData(parser);
        setLevelId(parser.level);
        setTotalPages(Math.ceil(parser.totalMembersCount / parser.pageSize));
    });

    useMessageEvent<GroupMemberUpdateEvent>(GroupMemberUpdateEvent, (event) => {
        if (event.getParser().groupId !== groupId) return;

        refreshMembers();
    });

    useMessageEvent<GroupMembersRefreshEvent>(GroupMembersRefreshEvent, (event) => {
        if (event.getParser().groupId !== groupId) return;

        refreshMembers();
    });

    // A deleted group closes its members window.
    useMessageEvent<HabboGroupDeactivatedMessageEvent>(HabboGroupDeactivatedMessageEvent, (event) => {
        if (event.getParser().groupId === groupId) setGroupId(-1);
    });

    useMessageEvent<GroupConfirmMemberRemoveEvent>(GroupConfirmMemberRemoveEvent, (event) => {
        const parser = event.getParser();
        const owned = claimReply(parser.userId);
        const pending = pendingRemovalRef.current;

        // Only the reply to this window's own request, for the group it still shows.
        if (!owned || !pending || pending.groupId !== owned.groupId || pending.userId !== owned.userId || pending.groupId !== groupId) return;

        pendingRemovalRef.current = null;

        const block = pending.block;
        const amount = parser.furnitureCount.toString();
        const name = pending.name ?? '';
        let text = LocalizeText(
            parser.furnitureCount > 0 ? 'group.kickconfirm.desc' : 'group.kickconfirm_nofurni.desc',
            ['user', 'amount'],
            [name, amount]
        );

        if (block) {
            text = (parser.furnitureCount > 0)
                ? localizeWithFallback('group.blockconfirm.desc', `${name} has ${amount} furni in the group home room. Block ${name} from the group?`, ['user', 'amount'], [name, amount])
                : localizeWithFallback('group.blockconfirm_nofurni.desc', `Block ${name} from the group? They can't request membership again until unblocked.`, ['user', 'amount'], [name, amount]);
        }

        let isSent = false;

        showConfirm(
            text,
            () => {
                // Valid only while this window still shows that group for the same signed-in user.
                if (isSent || pending.groupId !== groupIdRef.current || !isCurrentSession(owned)) return;

                isSent = true;
                SendMessageComposer(new GroupRemoveMemberComposer(pending.groupId, pending.userId, block));
            },
            null
        );
    });

    useEffect(() => {
        const linkTracker: ILinkEventTracker = {
            linkReceived: (url: string) => {
                const parts = url.split('/');

                if (parts.length < 2) return;

                const groupId = parseInt(parts[1]) || -1;
                const levelId = Number.isInteger(parseInt(parts[2])) ? parseInt(parts[2]) : 0;

                setGroupId(groupId);
                setLevelId(levelId);
                setPageId(0);
            },
            eventUrlPrefix: 'group-members/'
        };

        AddLinkEventTracker(linkTracker);

        return () => RemoveLinkEventTracker(linkTracker);
    }, []);

    useEffect(() => {
        setPageId(0);
    }, [groupId, levelId, searchQuery]);

    useEffect(() => refreshMembers(), [refreshMembers]);

    useEffect(() => () => membersRequests.cancel(), [membersRequests]);

    useEffect(() => {
        if (groupId === -1) return;

        setMembersData(null);
        setTotalPages(0);
        setSearchQuery('');
        pendingRemovalRef.current = null;
    }, [groupId]);

    if (groupId === -1 || !membersData) return null;

    return (
        <NativeTextTree>
            <OctaneCardView className="octane-groups-window octane-group-members" theme="primary-slim" isResizable={false}>
                <OctaneCardHeaderView
                    headerText={LocalizeText('group.members.title', ['groupName'], [membersData ? membersData.groupTitle : ''])}
                    onCloseClick={(event) => setGroupId(-1)}
                />
                <OctaneCardContentView className="octane-groups-content" overflow="hidden">
                    <div className="octane-group-members-search flex gap-2">
                        <Flex center className="group-badge octane-group-members-search__badge">
                            <LayoutBadgeImageView badgeCode={membersData.badge} className="mx-auto block" isGroup={true} />
                        </Flex>
                        <Column fullWidth gap={1} className="octane-group-members-search__controls">
                            <input
                                className="octane-groups-input min-h-[calc(1.5em+.5rem+2px)] px-[.5rem] py-[.25rem] text-[.7875rem] rounded-[.2rem] w-full"
                                placeholder={LocalizeText('group.members.searchinfo')}
                                type="text"
                                value={searchQuery}
                                onChange={(event) => setSearchQuery(event.target.value)}
                            />
                            <Select className="octane-groups-select form-select form-select-sm w-full" value={levelId} onChange={(event) => setLevelId(parseInt(event.target.value))}>
                                <option value="0">{LocalizeText('group.members.search.all')}</option>
                                <option value="1">{LocalizeText('group.members.search.admins')}</option>
                                {membersData.admin && <option value="2">{LocalizeText('group.members.search.pending')}</option>}
                                {membersData.admin && blockingEnabled && <option value="3">{localizeWithFallback('group.members.search.blocked', 'Show blocked users')}</option>}
                            </Select>
                        </Column>
                    </div>
                    <Grid className="octane-group-members-list-grid" columnCount={2} overflow="auto">
                        {membersData.result.map((member, index) => {
                            return (
                                <Flex key={index} alignItems="center" className="octane-group-member-row" gap={0} overflow="hidden">
                                    <BaseButton type="button" className="octane-group-member-row__avatar cursor-pointer" onClick={() => GetUserProfile(member.id)}>
                                        <LayoutAvatarImageView
                                            className="octane-group-member-row__head"
                                            direction={2}
                                            figure={member.figure}
                                            headOnly={true}
                                            compactHead
                                            compactHeadSize={40}
                                            compactHeadPadding={0}
                                        />
                                    </BaseButton>
                                    <Column className="octane-group-member-row__copy" grow gap={0}>
                                        <Text component={BaseButton} type="button" bold pointer small className="octane-group-member-row__name" onClick={(event) => GetUserProfile(member.id)}>
                                            {member.name}
                                        </Text>
                                        {member.rank !== GroupRank.REQUESTED && (
                                            <Text italics small variant="muted" className="octane-group-member-row__since">
                                                {LocalizeText('group.members.since', ['date'], [member.joinedAt])}
                                            </Text>
                                        )}
                                    </Column>
                                    <div className="octane-group-member-row__actions">
                                        {member.rank !== GroupRank.REQUESTED && member.rank !== GroupRank.BLOCKED && (
                                            <div className="flex items-center justify-center">
                                                <Hint content={LocalizeText(getRankDescription(member))}><BaseButton type="button" aria-label={LocalizeText(getRankDescription(member))}
                                                    className={classNames(
                                                        `octane-icon icon-group-small-${member.rank === GroupRank.OWNER ? 'owner' : member.rank === GroupRank.ADMIN ? 'admin' : membersData.admin && member.rank === GroupRank.MEMBER ? 'not-admin' : ''}`,
                                                        membersData.admin && 'cursor-pointer'
                                                    )}
                                                    onClick={(event) => toggleAdmin(member)}
                                                /></Hint>
                                            </div>
                                        )}
                                        {membersData.admin && member.rank === GroupRank.REQUESTED && (
                                            <Flex alignItems="center">
                                                <Hint content={LocalizeText('group.members.accept')}><BaseButton type="button" aria-label={LocalizeText('group.members.accept')}
                                                    className="cursor-pointer octane-friends-spritesheet icon-accept"
                                                    onClick={(event) => acceptMembership(member)}
                                                /></Hint>
                                            </Flex>
                                        )}
                                        {membersData.admin && member.rank === GroupRank.BLOCKED && (
                                            <Flex alignItems="center">
                                                <Hint content={localizeWithFallback('group.members.unblock', 'Unblock')}><BaseButton aria-label={localizeWithFallback('group.members.unblock', 'Unblock')}
                                                    className="octane-group-member-row__action cursor-pointer text-[#1e7295] hover:text-[#0d4a63]"
                                                    type="button"
                                                    onClick={() => unblockMember(member)}
                                                >
                                                    <FaUndo size={11} />
                                                </BaseButton></Hint>
                                            </Flex>
                                        )}
                                        {membersData.admin && blockingEnabled && (member.rank === GroupRank.MEMBER || member.rank === GroupRank.ADMIN) && member.id !== GetSessionDataManager().userId && (
                                            <Flex alignItems="center">
                                                <Hint content={localizeWithFallback('group.members.block', 'Block from group')}><BaseButton aria-label={localizeWithFallback('group.members.block', 'Block from group')}
                                                    className="octane-group-member-row__action cursor-pointer text-[#a81a12] hover:text-[#6f0f0a]"
                                                    type="button"
                                                    onClick={() => blockMember(member)}
                                                >
                                                    <FaBan size={11} />
                                                </BaseButton></Hint>
                                            </Flex>
                                        )}
                                        {membersData.admin && member.rank !== GroupRank.OWNER && member.rank !== GroupRank.BLOCKED && member.id !== GetSessionDataManager().userId && (
                                            <Flex alignItems="center">
                                                <Hint content={LocalizeText(member.rank === GroupRank.REQUESTED ? 'group.members.reject' : 'group.members.kick')}><BaseButton type="button" aria-label={LocalizeText(member.rank === GroupRank.REQUESTED ? 'group.members.reject' : 'group.members.kick')}
                                                    className="cursor-pointer octane-friends-spritesheet icon-deny"
                                                    onClick={(event) => removeMemberOrDeclineMembership(member)}
                                                /></Hint>
                                            </Flex>
                                        )}
                                    </div>
                                </Flex>
                            );
                        })}
                    </Grid>
                    {membersData.admin && levelId === 2 && membersData.result.length > 0 && (
                        <Button className="octane-groups-button" onClick={acceptAllMemberships}>
                            {localizeWithFallback('group.members.acceptall', 'Accept all')}
                        </Button>
                    )}
                    <Flex alignItems="center" gap={1} justifyContent="between" className="octane-groups-footer octane-group-members-footer">
                        <Button className="octane-groups-button octane-groups-button--pager" disabled={pageId <= 0} onClick={(event) => setPageId((prevValue) => Math.max(0, prevValue - 1))}>
                            <FaChevronLeft className="fa-icon" />
                        </Button>
                        <div className="octane-group-members-footer__page">
                            <Text small className="octane-group-members-footer__label">
                                {localizeWithFallback('group.members.members', `Members (${membersData.totalMembersCount})`, ['count'], [membersData.totalMembersCount.toString()])}
                            </Text>
                            <NumberInput className="octane-group-members-footer__input" min={1} max={Math.max(1, totalPages)} value={membersData.pageIndex + 1} onChange={(event) => {
                                    const value = Math.min(Math.max(parseInt(event.target.value) || 1, 1), Math.max(1, totalPages));
                                    setPageId(value - 1);
                                }} />
                            <Text small className="octane-group-members-footer__total">
                                / {Math.max(1, totalPages)}
                            </Text>
                        </div>
                        <Button
                            className="octane-groups-button octane-groups-button--pager"
                            disabled={totalPages === 0 || pageId >= totalPages - 1}
                            onClick={(event) => setPageId((prevValue) => Math.min(totalPages - 1, prevValue + 1))}
                        >
                            <FaChevronRight className="fa-icon" />
                        </Button>
                    </Flex>
                </OctaneCardContentView>
            </OctaneCardView>
        </NativeTextTree>
    );
};
