import {
    GetEventDispatcher,
    GetRoomSessionManager,
    GetSessionDataManager,
    GetSoundManager,
    IRoomSessionSnapshot,
    IRoomUserData,
    ISoundVolumesSnapshot,
    IUserDataSnapshot,
    OctaneEventType
} from '@octane/renderer';
import { useMemo } from 'react';
import { useExternalSnapshot } from '../events/useExternalSnapshot';

/**
 * React-side consumers for the referentially-stable snapshot getters
 * the renderer exposes (Octane Renderer v2.1.0+ pattern).
 *
 * Every hook here is a thin `useSyncExternalStore` wrapper: it subscribes
 * to the corresponding `OctaneEventType.*_UPDATED` invalidation event and
 * reads the matching `getXxxSnapshot()`. Because the renderer guarantees
 * snapshot reference invariance until invalidation, React's bailout logic
 * skips re-renders when the snapshot is unchanged — so widgets that read
 * the same slice across many components share a single subscription and
 * only re-paint when the underlying state actually changes.
 *
 * Prefer these over reaching into the manager directly with
 * `GetSessionDataManager().userId` etc., which never trigger a re-render
 * when the value changes.
 *
 * The hooks are intentionally defensive: every call site is wrapped in a
 * "is this method available?" guard so a renderer-version mismatch
 * (e.g. a stale `dist/` shadowing the source) degrades to a stable empty
 * fallback instead of crashing the React tree with `(intermediate value)()
 * is undefined` during the first paint.
 */

// Module-level frozen defaults — referentially stable so the React bailout
// keeps working when a manager method is unavailable.
const NOOP_UNSUBSCRIBE = (): void => undefined;

const DEFAULT_USER_DATA: Readonly<IUserDataSnapshot> = Object.freeze({
    userId: 0,
    userName: '',
    figure: '',
    gender: '',
    realName: '',
    respectsReceived: 0,
    respectsLeft: 0,
    respectsPetLeft: 0,
    canChangeName: false,
    clubLevel: 0,
    securityLevel: 0,
    isAmbassador: false,
    isEmailVerified: false,
    isNoob: false,
    isAuthenticHabbo: false,
    isSystemOpen: false,
    isSystemShutdown: false,
    uiFlags: 0,
    tags: Object.freeze<string[]>([]) as ReadonlyArray<string>,
    rankId: 0,
    rankName: '',
    rankBadge: '',
    rankPrefix: '',
    rankPrefixColor: ''
}) as Readonly<IUserDataSnapshot>;

const EMPTY_IGNORED_LIST: ReadonlyArray<number> = Object.freeze<number[]>([]) as ReadonlyArray<number>;
const EMPTY_BLOCKED_LIST: ReadonlyArray<number> = Object.freeze<number[]>([]) as ReadonlyArray<number>;
const EMPTY_GROUP_BADGES: ReadonlyMap<number, string> = new Map();
const EMPTY_USER_LIST: ReadonlyArray<IRoomUserData> = Object.freeze<IRoomUserData[]>([]) as ReadonlyArray<IRoomUserData>;
const EMPTY_PERMISSIONS: ReadonlyMap<string, number> = new Map();

const DEFAULT_VOLUMES: Readonly<ISoundVolumesSnapshot> = Object.freeze({
    system: 0.5,
    furni: 0.5,
    trax: 0.5
}) as Readonly<ISoundVolumesSnapshot>;

const subscribeTo =
    (eventType: string) =>
    (onChange: () => void): (() => void) => {
        const dispatcher = GetEventDispatcher();

        // Stale renderer (no v2.1.0 subscribe API) — return a no-op
        // unsubscribe so useSyncExternalStore stays mounted cleanly.
        if (!dispatcher || typeof dispatcher.subscribe !== 'function') return NOOP_UNSUBSCRIBE;

        return dispatcher.subscribe(eventType, onChange);
    };

export const useUserDataSnapshot = (): Readonly<IUserDataSnapshot> =>
    useExternalSnapshot(subscribeTo(OctaneEventType.SESSION_DATA_UPDATED), () => {
        const manager = GetSessionDataManager();

        if (!manager || typeof manager.getUserDataSnapshot !== 'function') return DEFAULT_USER_DATA;

        return manager.getUserDataSnapshot();
    });

export const useActiveRoomSessionSnapshot = (): Readonly<IRoomSessionSnapshot> | null =>
    useExternalSnapshot(subscribeTo(OctaneEventType.ROOM_SESSION_UPDATED), () => {
        const manager = GetRoomSessionManager();

        if (!manager || typeof manager.getActiveRoomSessionSnapshot !== 'function') return null;

        return manager.getActiveRoomSessionSnapshot();
    });

export const useIgnoredUsersSnapshot = (): ReadonlyArray<number> =>
    useExternalSnapshot(subscribeTo(OctaneEventType.IGNORED_USERS_UPDATED), () => {
        const inner = GetSessionDataManager()?.ignoredUsersManager;

        if (!inner || typeof inner.getIgnoredUsersSnapshot !== 'function') return EMPTY_IGNORED_LIST;

        return inner.getIgnoredUsersSnapshot();
    });

/**
 * Official `BlockedUsersManager` list (packets 485 / 697 / 1886 / 2649): the block list is
 * keyed by user id and is separate from the ignore list.
 */
export const useBlockedUsersSnapshot = (): ReadonlyArray<number> =>
    useExternalSnapshot(subscribeTo(OctaneEventType.BLOCKED_USERS_UPDATED), () => {
        const inner = GetSessionDataManager()?.blockedUsersManager;

        if (!inner || typeof inner.getBlockedUsersSnapshot !== 'function') return EMPTY_BLOCKED_LIST;

        return inner.getBlockedUsersSnapshot();
    });

/** Reactive predicate built on top of `useBlockedUsersSnapshot`. */
export const useIsUserBlocked = (userId: number): boolean => {
    const list = useBlockedUsersSnapshot();

    return useMemo(() => list.includes(userId), [list, userId]);
};

/**
 * Reactive predicate built on top of `useIgnoredUsersSnapshot`.
 * Re-renders only when the array reference flips (i.e. someone is added
 * or removed) — not on unrelated session updates.
 */
export const useIsUserIgnored = (userId: number): boolean => {
    const list = useIgnoredUsersSnapshot();

    return useMemo(() => list.includes(userId), [list, userId]);
};

/**
 * Reactive view of the current user's rank metadata — name, badge,
 * prefix, prefix color — mirrored from `permission_ranks` via the
 * extended `UserPermissionsComposer` wire (Arcturus ≥ 4.2.10). Use
 * this in PRESENTATIONAL code only (chat prefix coloring, badge in
 * the avatar overlay, "rank" line in the user profile). DO NOT use
 * it for gating UI capabilities: prefer the permission-based family
 * (`useHasPermission(key)`) below, which is dynamic against
 * `permission_definitions` and survives rank renumbering.
 */
export interface IUserRank {
    readonly id: number;
    readonly name: string;
    readonly level: number;
    readonly badge: string;
    readonly prefix: string;
    readonly prefixColor: string;
}

export const useUserRank = (): IUserRank => {
    const userData = useUserDataSnapshot();

    return useMemo<IUserRank>(
        () => ({
            id: userData.rankId,
            name: userData.rankName,
            level: userData.securityLevel,
            badge: userData.rankBadge,
            prefix: userData.rankPrefix,
            prefixColor: userData.rankPrefixColor
        }),
        [userData.rankId, userData.rankName, userData.securityLevel, userData.rankBadge, userData.rankPrefix, userData.rankPrefixColor]
    );
};

/**
 * Resolved permission map for the current user, mirroring
 * `permission_definitions` filtered to the user's rank. Backed by
 * `SessionDataManager.getPermissionsSnapshot()` and invalidated by
 * `USER_PERMISSIONS_UPDATED` (Arcturus dispatches the underlying
 * packet at login + after every `setRank`).
 *
 * Values: 1 = ALLOWED, 2 = ROOM_OWNER (legacy gate that requires
 * the user to also be the room owner). Absent key = DISALLOWED.
 *
 * Empty Map when the connected emulator doesn't ship the extension
 * (older deployments) — `useHasPermission` then returns false for
 * every key, which hides mod-only UI by default (safe).
 */
export const useUserPermissions = (): ReadonlyMap<string, number> =>
    useExternalSnapshot(subscribeTo(OctaneEventType.USER_PERMISSIONS_UPDATED), () => {
        const manager = GetSessionDataManager();

        if (!manager || typeof manager.getPermissionsSnapshot !== 'function') return EMPTY_PERMISSIONS;

        return manager.getPermissionsSnapshot();
    });

/**
 * Reactive predicate: does the current user have the named
 * permission **unconditionally** (ALLOWED only)? `key` must match a
 * row in `permission_definitions.permission_key` (e.g.
 * `'acc_supporttool'`, `'acc_anyroomowner'`, `'acc_catalogfurni'`).
 *
 * Mirrors the server-side semantics of `Habbo.hasPermission(key)`
 * (PermissionsManager → `Rank.hasPermission(key, isRoomOwner=false)`
 * which returns `setting == ALLOWED` — `setting == ROOM_OWNER`
 * requires the call site to pass `isRoomOwner=true`, which the
 * client doesn't have ambiently). So this hook returns `true`
 * only for ALLOWED (value 1) and `false` for ROOM_OWNER (value 2)
 * — the latter has to be re-checked against the active room
 * ownership via `usePermissionValue(key) === 2 && roomSession.isRoomOwner`.
 *
 * Prefer this over any rank-based gate — it survives rank
 * renumbering and adding new ranks without touching the React code.
 */
export const useHasPermission = (key: string): boolean => {
    const permissions = useUserPermissions();

    return useMemo(() => permissions.get(key) === 1, [permissions, key]);
};

/**
 * Reactive raw permission value:
 *   0 = DISALLOWED (also returned for absent keys)
 *   1 = ALLOWED
 *   2 = ROOM_OWNER (granted only when the caller is the active
 *       room owner — combine with `roomSession.isRoomOwner`)
 *
 * Use this when the gate needs to distinguish ROOM_OWNER from
 * plain ALLOWED, or for the handful of permissions whose
 * `permission_definitions.max_value > 1`.
 */
export const usePermissionValue = (key: string): number => {
    const permissions = useUserPermissions();

    return useMemo(() => permissions.get(key) ?? 0, [permissions, key]);
};

/**
 * Reactive ambassador flag. Alias of
 * `useHasPermission('acc_ambassador')` — the snapshot also carries
 * an explicit `isAmbassador` boolean (legacy
 * `UserPermissionsComposer` field), but routing it through the
 * permission map keeps a single source of truth for runtime
 * promote/demote.
 */
export const useIsAmbassador = (): boolean => useHasPermission('acc_ambassador');

export const useGroupBadgesSnapshot = (): ReadonlyMap<number, string> =>
    useExternalSnapshot(subscribeTo(OctaneEventType.GROUP_BADGES_UPDATED), () => {
        const inner = GetSessionDataManager()?.groupInformationManager;

        if (!inner || typeof inner.getGroupBadgesSnapshot !== 'function') return EMPTY_GROUP_BADGES;

        return inner.getGroupBadgesSnapshot();
    });

/**
 * Returns the badge id for a given group, reactive. Empty string when
 * the badge isn't known (matches the legacy `getGroupBadge` fallback).
 */
export const useGroupBadge = (groupId: number): string => {
    const badges = useGroupBadgesSnapshot();

    return useMemo(() => badges.get(groupId) ?? '', [badges, groupId]);
};

export const useVolumesSnapshot = (): Readonly<ISoundVolumesSnapshot> =>
    useExternalSnapshot(subscribeTo(OctaneEventType.SOUND_VOLUMES_UPDATED), () => {
        const manager = GetSoundManager();

        if (!manager || typeof manager.getVolumesSnapshot !== 'function') return DEFAULT_VOLUMES;

        return manager.getVolumesSnapshot();
    });

/**
 * Returns the active room's user list, reactive. Returns an empty
 * frozen array when no room session is active (or when the renderer
 * doesn't expose the snapshot getter yet).
 *
 * Subscribes to BOTH `ROOM_USER_LIST_UPDATED` (join/leave/update inside
 * the active session) AND `ROOM_SESSION_UPDATED` (because the underlying
 * `userDataManager` reference flips when the active room changes).
 */
export const useRoomUserListSnapshot = (): ReadonlyArray<IRoomUserData> =>
    useExternalSnapshot(
        (onChange) => {
            const dispatcher = GetEventDispatcher();

            if (!dispatcher || typeof dispatcher.subscribe !== 'function') return NOOP_UNSUBSCRIBE;

            const offList = dispatcher.subscribe(OctaneEventType.ROOM_USER_LIST_UPDATED, onChange);
            const offSession = dispatcher.subscribe(OctaneEventType.ROOM_SESSION_UPDATED, onChange);

            return () => {
                offList();
                offSession();
            };
        },
        () => {
            const userDataManager = GetRoomSessionManager()?.getActiveRoomSessionSnapshot?.()?.session?.userDataManager;

            if (!userDataManager || typeof userDataManager.getRoomUserListSnapshot !== 'function') return EMPTY_USER_LIST;

            return userDataManager.getRoomUserListSnapshot();
        }
    );
