/*
Copyright 2026 Element Creations Ltd.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import {
    BaseViewModel,
    type RoomListSnapshot,
    type FilterId,
    type RoomListViewActions,
    type RoomListViewState,
} from "@element-hq/web-shared-components";
import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";

import { Action } from "../../dispatcher/actions";
import dispatcher from "../../dispatcher/dispatcher";
import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload";
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import SpaceStore from "../../stores/spaces/SpaceStore";
import RoomListStoreV3, { RoomListStoreV3Event, type RoomsResult } from "../../stores/room-list-v3/RoomListStoreV3";
import { FilterKey } from "../../stores/room-list-v3/skip-list/filters";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { RoomListItemViewModel } from "./RoomListItemViewModel";
import { SdkContextClass } from "../../contexts/SDKContext";
import { hasCreateRoomRights } from "./utils";
import { keepIfSame } from "../../utils/keepIfSame";

interface RoomListViewViewModelProps {
    client: MatrixClient;
}

const filterKeyToIdMap: Map<FilterKey, FilterId> = new Map([
    [FilterKey.UnreadFilter, "unread"],
    [FilterKey.PeopleFilter, "people"],
    [FilterKey.RoomsFilter, "rooms"],
    [FilterKey.FavouriteFilter, "favourite"],
    [FilterKey.MentionsFilter, "mentions"],
    [FilterKey.InvitesFilter, "invites"],
    [FilterKey.LowPriorityFilter, "low_priority"],
]);

export class RoomListViewViewModel
    extends BaseViewModel<RoomListSnapshot, RoomListViewViewModelProps>
    implements RoomListViewActions
{
    // State tracking
    private activeFilter: FilterKey | undefined = undefined;
    private roomsResult: RoomsResult;
    private lastActiveRoomIndex: number | undefined = undefined;

    // Child view model management
    private roomItemViewModels = new Map<string, RoomListItemViewModel>();
    // This map is intentionally additive (never cleared except on space changes) to avoid a race condition:
    // a list update can refresh roomsResult and roomsMap before the view re-renders, so the view may still
    // request a view model for a room that was removed from the latest list. Keeping old entries prevents a crash.
    private roomsMap = new Map<string, Room>();

    public constructor(props: RoomListViewViewModelProps) {
        const activeSpace = SpaceStore.instance.activeSpaceRoom;

        // Get initial rooms
        const roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(undefined);
        const canCreateRoom = hasCreateRoomRights(props.client, activeSpace);
        const filterIds = [...filterKeyToIdMap.values()];
        const roomIds = roomsResult.rooms.map((room) => room.roomId);
        const sections = [{ id: "all", roomIds }];

        super(props, {
            // Initial view state - start with empty, will populate in async init
            isLoadingRooms: RoomListStoreV3.instance.isLoadingRooms,
            isRoomListEmpty: roomsResult.rooms.length === 0,
            filterIds,
            activeFilterId: undefined,
            roomListState: {
                activeRoomIndex: undefined,
                spaceId: roomsResult.spaceId,
                filterKeys: undefined,
            },
            // Until we implement sections, this view model only supports the flat list mode
            isFlatList: true,
            sections,
            canCreateRoom,
        });

        this.roomsResult = roomsResult;

        // Build initial roomsMap from roomsResult
        this.updateRoomsMap(roomsResult);

        // Subscribe to room list updates
        this.disposables.trackListener(
            RoomListStoreV3.instance,
            RoomListStoreV3Event.ListsUpdate as any,
            this.onListsUpdate,
        );

        // Subscribe to room list loaded
        this.disposables.trackListener(
            RoomListStoreV3.instance,
            RoomListStoreV3Event.ListsLoaded as any,
            this.onListsLoaded,
        );

        // Subscribe to active room changes to update selected room
        const dispatcherRef = dispatcher.register(this.onDispatch);
        this.disposables.track(() => {
            dispatcher.unregister(dispatcherRef);
        });

        // Track cleanup of all child view models
        this.disposables.track(() => {
            for (const viewModel of this.roomItemViewModels.values()) {
                viewModel.dispose();
            }
            this.roomItemViewModels.clear();
        });
    }

    public onToggleFilter = (filterId: FilterId): void => {
        // Find the FilterKey by matching the filter ID
        let filterKey: FilterKey | undefined = undefined;
        for (const [key, id] of filterKeyToIdMap.entries()) {
            if (id === filterId) {
                filterKey = key;
                break;
            }
        }

        if (filterKey === undefined) return;

        // Toggle the filter - if it's already active, deactivate it
        const newFilter = this.activeFilter === filterKey ? undefined : filterKey;
        this.activeFilter = newFilter;

        // Update rooms result with new filter
        const filterKeys = this.activeFilter !== undefined ? [this.activeFilter] : undefined;
        this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filterKeys);

        // Update roomsMap immediately before clearing VMs
        this.updateRoomsMap(this.roomsResult);

        this.updateRoomListData();
    };

    /**
     * Add rooms from the RoomsResult to the roomsMap for quick lookup.
     * This does not clear the roomsMap.
     * This maintains a quick lookup for room objects.
     */
    private updateRoomsMap(roomsResult: RoomsResult): void {
        for (const room of roomsResult.rooms) {
            this.roomsMap.set(room.roomId, room);
        }
    }

    /**
     * Clear all child view models.
     * Called when the room list structure changes (space change, filter change, etc.)
     */
    private clearViewModels(): void {
        for (const viewModel of this.roomItemViewModels.values()) {
            viewModel.dispose();
        }
        this.roomItemViewModels.clear();
    }

    /**
     * Get the ordered list of room IDs.
     */
    public get roomIds(): string[] {
        return this.roomsResult.rooms.map((room) => room.roomId);
    }

    /**
     * Get a RoomListItemViewModel for a specific room.
     * Creates a RoomListItemViewModel if needed, which manages per-room subscriptions.
     * The view should call this only for visible rooms from the roomIds list.
     * @throws Error if room is not found in roomsMap (indicates a programming error)
     */
    public getRoomItemViewModel(roomId: string): RoomListItemViewModel {
        // Check if we have a view model for this room
        let viewModel = this.roomItemViewModels.get(roomId);

        if (!viewModel) {
            let room = this.roomsMap.get(roomId);
            if (!room) {
                // Maybe the roomsMap is out of date due to a recent roomsResult change that hasn't been applied yet (race condition)
                this.updateRoomsMap(this.roomsResult);
                room = this.roomsMap.get(roomId);
            }

            if (!room) throw new Error(`Room ${roomId} not found in roomsMap`);

            // Create new view model
            viewModel = new RoomListItemViewModel({
                room,
                client: this.props.client,
            });

            this.roomItemViewModels.set(roomId, viewModel);
        }

        // Return the view model - the view will call useViewModel() on it
        return viewModel;
    }

    /**
     * Not implemented - this view model does not support sections.
     * Flat list mode is forced so this method is never be called.
     * @throw Error if called
     */
    public getSectionHeaderViewModel(): never {
        throw new Error("Sections are not supported in this room list");
    }

    /**
     * Update which rooms are currently visible.
     * Called by the view when scroll position changes.
     * Disposes of view models for rooms no longer visible.
     */
    public updateVisibleRooms(startIndex: number, endIndex: number): void {
        const allRoomIds = this.roomIds;
        const newVisibleIds = allRoomIds.slice(startIndex, Math.min(endIndex, allRoomIds.length));

        const newVisibleSet = new Set(newVisibleIds);

        // Dispose view models for rooms no longer visible
        for (const [roomId, viewModel] of this.roomItemViewModels.entries()) {
            if (!newVisibleSet.has(roomId)) {
                viewModel.dispose();
                this.roomItemViewModels.delete(roomId);
            }
        }
    }

    private onDispatch = (payload: any): void => {
        if (payload.action === Action.ActiveRoomChanged) {
            // When the active room changes, update the room list data to reflect the new selected room
            // Pass isRoomChange=true so sticky logic doesn't prevent the index from updating
            this.updateRoomListData(true);
        } else if (payload.action === Action.ViewRoomDelta) {
            // Handle keyboard navigation shortcuts (Alt+ArrowUp/Down)
            // This was previously handled by useRoomListNavigation hook
            this.handleViewRoomDelta(payload as ViewRoomDeltaPayload);
        }
    };

    /**
     * Handle keyboard navigation shortcuts (Alt+ArrowUp/Down) to move between rooms.
     * Supports both regular navigation and unread-only navigation.
     * Migrated from useRoomListNavigation hook.
     */
    private handleViewRoomDelta(payload: ViewRoomDeltaPayload): void {
        const currentRoomId = SdkContextClass.instance.roomViewStore.getRoomId();
        if (!currentRoomId) return;

        const { delta, unread } = payload;
        const rooms = this.roomsResult.rooms;

        const filteredRooms = unread
            ? // Filter the rooms to only include unread ones and the active room
              rooms.filter((room) => {
                  const state = RoomNotificationStateStore.instance.getRoomState(room);
                  return room.roomId === currentRoomId || state.isUnread;
              })
            : rooms;

        const currentIndex = filteredRooms.findIndex((room) => room.roomId === currentRoomId);
        if (currentIndex === -1) return;

        // Get the next/previous new room according to the delta
        // Use slice to loop on the list
        // If delta is -1 at the start of the list, it will go to the end
        // If delta is 1 at the end of the list, it will go to the start
        const [newRoom] = filteredRooms.slice((currentIndex + delta) % filteredRooms.length);
        if (!newRoom) return;

        dispatcher.dispatch<ViewRoomPayload>({
            action: Action.ViewRoom,
            room_id: newRoom.roomId,
            show_room_tile: true, // to make sure the room gets scrolled into view
            metricsTrigger: "WebKeyboardShortcut",
            metricsViaKeyboard: true,
        });
    }

    /**
     * Handle room list updates from RoomListStoreV3.
     *
     * This event fires when:
     * - Room order changes (new messages, manual reordering)
     * - Active space changes
     * - Filters are applied
     * - Rooms are added/removed
     *
     * Space changes are detected by comparing old vs new spaceId.
     * This matches the old hook pattern where space changes were handled
     * indirectly through room list updates.
     */
    private onListsUpdate = (): void => {
        const filterKeys = this.activeFilter !== undefined ? [this.activeFilter] : undefined;
        const oldSpaceId = this.roomsResult.spaceId;

        // Refresh room data from store
        this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filterKeys);
        const newSpaceId = this.roomsResult.spaceId;

        // Detect space change
        if (oldSpaceId !== newSpaceId) {
            // Clear view models when the space changes
            // We only want to do this on space changes, not on regular list updates, to preserve view models when possible
            // The view models are disposed when scrolling out of view (handled by updateVisibleRooms)
            this.clearViewModels();
            // Clear roomsMap to prevent stale room data - it will be repopulated with the new roomsResult
            this.roomsMap.clear();

            this.updateRoomsMap(this.roomsResult);

            // Space changed - get the last selected room for the new space to prevent flicker
            const lastSelectedRoom = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpaceId);

            this.updateRoomListData(true, lastSelectedRoom);
            return;
        }

        this.updateRoomsMap(this.roomsResult);

        // Normal room list update (not a space change)
        this.updateRoomListData();
    };

    private onListsLoaded = (): void => {
        // Room lists have finished loading
        this.snapshot.merge({
            isLoadingRooms: false,
        });
    };

    /**
     * Calculate the active room index based on the currently viewed room.
     * Returns undefined if no room is selected or if the selected room is not in the current list.
     *
     * @param roomId - The room ID to find the index for (can be null/undefined)
     */
    private getActiveRoomIndex(roomId: string | null | undefined): number | undefined {
        if (!roomId) {
            return undefined;
        }

        const index = this.roomsResult.rooms.findIndex((room) => room.roomId === roomId);
        return index >= 0 ? index : undefined;
    }

    /**
     * Apply sticky room logic to keep the active room at the same index position.
     * When the room list updates, this prevents the selected room from jumping around in the UI.
     *
     * @param isRoomChange - Whether this update is due to a room change (not a list update)
     * @param roomId - The room ID to apply sticky logic for (can be null/undefined)
     * @returns The modified rooms array with sticky positioning applied
     */
    private applyStickyRoom(isRoomChange: boolean, roomId: string | null | undefined): Room[] {
        const rooms = this.roomsResult.rooms;

        if (!roomId) {
            return rooms;
        }

        const newIndex = rooms.findIndex((room) => room.roomId === roomId);
        const oldIndex = this.lastActiveRoomIndex;

        // When opening another room, the index should obviously change
        if (isRoomChange) {
            return rooms;
        }

        // If oldIndex is undefined, then there was no active room before
        // Similarly, if newIndex is -1, the active room is not in the current list
        if (newIndex === -1 || oldIndex === undefined) {
            return rooms;
        }

        // If the index hasn't changed, we have nothing to do
        if (newIndex === oldIndex) {
            return rooms;
        }

        // If the old index falls out of the bounds of the rooms array
        // (usually because rooms were removed), we can no longer place
        // the active room in the same old index
        if (oldIndex > rooms.length - 1) {
            return rooms;
        }

        // Making the active room sticky is as simple as removing it from
        // its new index and placing it in the old index
        const newRooms = [...rooms];
        const [stickyRoom] = newRooms.splice(newIndex, 1);
        newRooms.splice(oldIndex, 0, stickyRoom);

        return newRooms;
    }

    private async updateRoomListData(
        isRoomChange: boolean = false,
        roomIdOverride: string | null = null,
    ): Promise<void> {
        // Determine the room ID to use for calculations
        // Use override if provided (e.g., during space changes), otherwise fall back to RoomViewStore
        const roomId = roomIdOverride ?? SdkContextClass.instance.roomViewStore.getRoomId();

        // Apply sticky room logic to keep selected room at same position
        const stickyRooms = this.applyStickyRoom(isRoomChange, roomId);

        // Update roomsResult with sticky rooms
        this.roomsResult = {
            ...this.roomsResult,
            rooms: stickyRooms,
        };

        // Rebuild roomsMap with the reordered rooms
        this.updateRoomsMap(this.roomsResult);

        // Calculate the active room index after applying sticky logic
        const activeRoomIndex = this.getActiveRoomIndex(roomId);

        // Track the current active room index for future sticky calculations
        this.lastActiveRoomIndex = activeRoomIndex;

        // Build the complete state atomically to ensure consistency
        // roomIds and roomListState must always be in sync
        const roomIds = this.roomIds;
        const sections = [{ id: "all", roomIds }];

        // Update filter keys - only update if they have actually changed to prevent unnecessary re-renders of the room list
        const previousFilterKeys = this.snapshot.current.roomListState.filterKeys;
        const newFilterKeys = this.roomsResult.filterKeys?.map((k) => String(k));
        const roomListState: RoomListViewState = {
            activeRoomIndex,
            spaceId: this.roomsResult.spaceId,
            filterKeys: keepIfSame(previousFilterKeys, newFilterKeys),
        };

        const activeFilterId = this.activeFilter !== undefined ? filterKeyToIdMap.get(this.activeFilter) : undefined;
        const isRoomListEmpty = roomIds.length === 0;
        const isLoadingRooms = RoomListStoreV3.instance.isLoadingRooms;

        // Single atomic snapshot update
        this.snapshot.merge({
            isLoadingRooms,
            isRoomListEmpty,
            activeFilterId,
            roomListState,
            sections,
        });
    }

    public createChatRoom = (): void => {
        dispatcher.fire(Action.CreateChat);
    };

    public createRoom = (): void => {
        const activeSpace = SpaceStore.instance.activeSpaceRoom;
        if (activeSpace) {
            dispatcher.dispatch({
                action: Action.CreateRoom,
                parent_space: activeSpace,
            });
        } else {
            dispatcher.dispatch({
                action: Action.CreateRoom,
            });
        }
    };
}
