import { createReducer, on } from '@ngrx/store';
import { Room, RoomSettings } from '../../shared-kernel';
import { normalizeRoomAccessControl } from '../../domains/access-control';
import { type ServerInfo } from '../../domains/server-directory';
import { RoomsActions } from './rooms.actions';
import { defaultChannels } from './room-channels.defaults';
import {
  isChannelNameTaken,
  normalizeChannelName,
  normalizeRoomChannels
} from './room-channels.rules';
import { pruneRoomMembers } from './room-members.helpers';

/** Deduplicate rooms by id, keeping the last occurrence */
function deduplicateRooms(rooms: Room[]): Room[] {
  const seen = new Map<string, Room>();

  for (const room of rooms) {
    seen.set(room.id, room);
  }

  return Array.from(seen.values());
}

/** Normalize room defaults and prune any stale persisted member entries. */
function enrichRoom(room: Room): Room {
  return normalizeRoomAccessControl({
    ...room,
    hasPassword: typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password,
    channels: normalizeRoomChannels(room.channels) || defaultChannels(),
    members: pruneRoomMembers(room.members || [])
  });
}

function resolveActiveTextChannelId(channels: Room['channels'], currentActiveChannelId: string): string {
  const textChannels = (channels ?? []).filter((channel) => channel.type === 'text');

  return textChannels.some((channel) => channel.id === currentActiveChannelId) ? currentActiveChannelId : (textChannels[0]?.id ?? 'general');
}

function getDefaultTextChannelId(room: Room): string {
  return resolveActiveTextChannelId(enrichRoom(room).channels, 'general');
}

function activateRoomView(state: RoomsState, room: Room, isConnecting: boolean, updateSavedRooms = true): RoomsState {
  const enriched = enrichRoom(room);

  return {
    ...state,
    currentRoom: enriched,
    savedRooms: updateSavedRooms ? upsertRoom(state.savedRooms, enriched) : state.savedRooms,
    isConnecting,
    signalServerCompatibilityError: null,
    isConnected: true,
    activeChannelId: getDefaultTextChannelId(enriched)
  };
}

/** Upsert a room into a saved-rooms list (add or replace by id) */
function upsertRoom(savedRooms: Room[], room: Room): Room[] {
  const normalizedRoom = enrichRoom(room);
  const idx = savedRooms.findIndex((existingRoom) => existingRoom.id === room.id);

  if (idx >= 0) {
    const updated = [...savedRooms];

    updated[idx] = normalizedRoom;
    return updated;
  }

  return [...savedRooms, normalizedRoom];
}

/** State shape for the rooms feature slice. */
export interface RoomsState {
  /** The room the user is currently viewing. */
  currentRoom: Room | null;
  /** All rooms persisted locally (joined or created). */
  savedRooms: Room[];
  /** Editable settings for the current room. */
  roomSettings: RoomSettings | null;
  /** Results returned from the server directory search. */
  searchResults: ServerInfo[];
  /** Whether a server directory search is in progress. */
  isSearching: boolean;
  /** Whether a connection to a room is being established. */
  isConnecting: boolean;
  /** Whether the user is connected to a room. */
  isConnected: boolean;
  /** Whether the current room is using locally cached data while reconnecting. */
  isSignalServerReconnecting: boolean;
  /** Banner message when the viewed room's signaling endpoint is incompatible. */
  signalServerCompatibilityError: string | null;
  /** Whether rooms are being loaded from local storage. */
  loading: boolean;
  /** Most recent error message, if any. */
  error: string | null;
  /** ID of the currently selected text channel. */
  activeChannelId: string;
}

export const initialState: RoomsState = {
  currentRoom: null,
  savedRooms: [],
  roomSettings: null,
  searchResults: [],
  isSearching: false,
  isConnecting: false,
  isConnected: false,
  isSignalServerReconnecting: false,
  signalServerCompatibilityError: null,
  loading: false,
  error: null,
  activeChannelId: 'general'
};

export const roomsReducer = createReducer(
  initialState,

  on(RoomsActions.resetRoomsState, () => ({
    ...initialState
  })),

  // Load rooms
  on(RoomsActions.loadRooms, (state) => ({
    ...state,
    loading: true,
    error: null
  })),

  on(RoomsActions.loadRoomsSuccess, (state, { rooms }) => ({
    ...state,
    savedRooms: deduplicateRooms(rooms.map(enrichRoom)),
    loading: false
  })),

  on(RoomsActions.loadRoomsFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error
  })),

  // Search servers
  on(RoomsActions.searchServers, (state) => ({
    ...state,
    isSearching: true,
    error: null
  })),

  on(RoomsActions.searchServersSuccess, (state, { servers }) => ({
    ...state,
    searchResults: servers,
    isSearching: false
  })),

  on(RoomsActions.searchServersFailure, (state, { error }) => ({
    ...state,
    isSearching: false,
    error
  })),

  // Create room
  on(RoomsActions.createRoom, (state) => ({
    ...state,
    isConnecting: true,
    signalServerCompatibilityError: null,
    error: null
  })),

  on(RoomsActions.createRoomSuccess, (state, { room }) => {
    const enriched = enrichRoom(room);

    return {
      ...state,
      currentRoom: enriched,
      savedRooms: upsertRoom(state.savedRooms, enriched),
      isConnecting: false,
      isSignalServerReconnecting: false,
      signalServerCompatibilityError: null,
      isConnected: true,
      activeChannelId: getDefaultTextChannelId(enriched)
    };
  }),

  on(RoomsActions.createRoomFailure, (state, { error }) => ({
    ...state,
    isConnecting: false,
    error
  })),

  // Join room
  on(RoomsActions.joinRoom, (state) => ({
    ...state,
    isConnecting: true,
    signalServerCompatibilityError: null,
    error: null
  })),

  on(RoomsActions.joinRoomSuccess, (state, { room }) => {
    const enriched = enrichRoom(room);

    return {
      ...state,
      currentRoom: enriched,
      savedRooms: upsertRoom(state.savedRooms, enriched),
      isConnecting: false,
      isSignalServerReconnecting: false,
      signalServerCompatibilityError: null,
      isConnected: true,
      activeChannelId: getDefaultTextChannelId(enriched)
    };
  }),

  on(RoomsActions.joinRoomFailure, (state, { error }) => ({
    ...state,
    isConnecting: false,
    error
  })),

  // Leave room
  on(RoomsActions.leaveRoom, (state) => ({
    ...state,
    isConnecting: true
  })),

  on(RoomsActions.leaveRoomSuccess, (state) => ({
    ...state,
    currentRoom: null,
    roomSettings: null,
    isSignalServerReconnecting: false,
    signalServerCompatibilityError: null,
    isConnecting: false,
    isConnected: false
  })),

  // View server - just switch the viewed room, stay connected
  on(RoomsActions.viewServer, (state, { room, skipBanCheck }) => {
    if (skipBanCheck) {
      return {
        ...activateRoomView(state, room, true, false),
        error: null
      };
    }

    return {
      ...state,
      isConnecting: true,
      signalServerCompatibilityError: null,
      error: null
    };
  }),

  on(RoomsActions.viewServerSuccess, (state, { room }) => activateRoomView(state, room, false, false)),

  // Update room settings
  on(RoomsActions.updateRoomSettings, (state) => ({
    ...state,
    error: null
  })),

  on(RoomsActions.updateRoomSettingsSuccess, (state, { roomId, settings }) => {
    const baseRoom = state.savedRooms.find((savedRoom) => savedRoom.id === roomId) || (state.currentRoom?.id === roomId ? state.currentRoom : null);

    if (!baseRoom) {
      return {
        ...state,
        roomSettings: state.currentRoom?.id === roomId ? settings : state.roomSettings
      };
    }

    const updatedRoom = enrichRoom({
      ...baseRoom,
      name: settings.name,
      description: settings.description,
      topic: settings.topic,
      isPrivate: settings.isPrivate,
      password: settings.password === '' ? undefined : (settings.password ?? baseRoom.password),
      hasPassword:
        typeof settings.hasPassword === 'boolean'
          ? settings.hasPassword
          : typeof settings.password === 'string'
            ? settings.password.trim().length > 0
            : baseRoom.hasPassword,
      maxUsers: settings.maxUsers
    });

    return {
      ...state,
      roomSettings: state.currentRoom?.id === roomId ? settings : state.roomSettings,
      currentRoom: state.currentRoom?.id === roomId ? updatedRoom : state.currentRoom,
      savedRooms: upsertRoom(state.savedRooms, updatedRoom)
    };
  }),

  on(RoomsActions.updateRoomSettingsFailure, (state, { error }) => ({
    ...state,
    error
  })),

  // Delete room
  on(RoomsActions.deleteRoomSuccess, (state, { roomId }) => ({
    ...state,
    isSignalServerReconnecting: state.currentRoom?.id === roomId ? false : state.isSignalServerReconnecting,
    signalServerCompatibilityError: state.currentRoom?.id === roomId ? null : state.signalServerCompatibilityError,
    savedRooms: state.savedRooms.filter((room) => room.id !== roomId),
    currentRoom: state.currentRoom?.id === roomId ? null : state.currentRoom
  })),

  // Forget room (local only)
  on(RoomsActions.forgetRoomSuccess, (state, { roomId }) => ({
    ...state,
    isSignalServerReconnecting: state.currentRoom?.id === roomId ? false : state.isSignalServerReconnecting,
    signalServerCompatibilityError: state.currentRoom?.id === roomId ? null : state.signalServerCompatibilityError,
    savedRooms: state.savedRooms.filter((room) => room.id !== roomId),
    currentRoom: state.currentRoom?.id === roomId ? null : state.currentRoom
  })),

  // Set current room
  on(RoomsActions.setCurrentRoom, (state, { room }) => ({
    ...state,
    currentRoom: enrichRoom(room),
    savedRooms: upsertRoom(state.savedRooms, room),
    isSignalServerReconnecting: false,
    signalServerCompatibilityError: null,
    isConnected: true,
    activeChannelId: getDefaultTextChannelId(room)
  })),

  // Clear current room
  on(RoomsActions.clearCurrentRoom, (state) => ({
    ...state,
    currentRoom: null,
    roomSettings: null,
    isSignalServerReconnecting: false,
    signalServerCompatibilityError: null,
    isConnected: false
  })),

  // Update room
  on(RoomsActions.updateRoom, (state, { roomId, changes }) => {
    const baseRoom = state.savedRooms.find((savedRoom) => savedRoom.id === roomId) || (state.currentRoom?.id === roomId ? state.currentRoom : null);

    if (!baseRoom)
      return state;

    const updatedRoom = enrichRoom({ ...baseRoom, ...changes });

    return {
      ...state,
      currentRoom: state.currentRoom?.id === roomId ? updatedRoom : state.currentRoom,
      savedRooms: upsertRoom(state.savedRooms, updatedRoom),
      activeChannelId:
        state.currentRoom?.id === roomId ? resolveActiveTextChannelId(updatedRoom.channels, state.activeChannelId) : state.activeChannelId
    };
  }),

  // Update server icon success
  on(RoomsActions.updateServerIconSuccess, (state, { roomId, icon, iconUpdatedAt }) => {
    const baseRoom = state.savedRooms.find((savedRoom) => savedRoom.id === roomId) || (state.currentRoom?.id === roomId ? state.currentRoom : null);

    if (!baseRoom)
      return state;

    const updatedRoom = enrichRoom({ ...baseRoom, icon, iconUpdatedAt });

    return {
      ...state,
      currentRoom: updatedRoom,
      savedRooms: upsertRoom(state.savedRooms, updatedRoom)
    };
  }),

  on(RoomsActions.receiveSearchServerIcon, (state, { roomId, icon, iconUpdatedAt }) => ({
    ...state,
    searchResults: state.searchResults.map((server) =>
      server.id === roomId && (!server.icon || (server.iconUpdatedAt ?? 0) < iconUpdatedAt) ? { ...server, icon, iconUpdatedAt } : server
    )
  })),

  // Receive room update
  on(RoomsActions.receiveRoomUpdate, (state, { room }) => {
    if (!state.currentRoom)
      return state;

    const updatedRoom = enrichRoom({ ...state.currentRoom, ...room });

    return {
      ...state,
      currentRoom: updatedRoom,
      savedRooms: upsertRoom(state.savedRooms, updatedRoom),
      activeChannelId: resolveActiveTextChannelId(updatedRoom.channels, state.activeChannelId)
    };
  }),

  // Clear search results
  on(RoomsActions.clearSearchResults, (state) => ({
    ...state,
    searchResults: []
  })),

  // Set connecting
  on(RoomsActions.setConnecting, (state, { isConnecting }) => ({
    ...state,
    isConnecting
  })),

  on(RoomsActions.setSignalServerReconnecting, (state, { isReconnecting }) => ({
    ...state,
    isSignalServerReconnecting: isReconnecting
  })),

  on(RoomsActions.setSignalServerCompatibilityError, (state, { message }) => ({
    ...state,
    signalServerCompatibilityError: message
  })),

  // Channel management
  on(RoomsActions.selectChannel, (state, { channelId }) => ({
    ...state,
    activeChannelId: channelId
  })),

  on(RoomsActions.addChannel, (state, { channel }) => {
    if (!state.currentRoom)
      return state;

    const existing = state.currentRoom.channels || defaultChannels();
    const normalizedName = normalizeChannelName(channel.name);

    if (!normalizedName || existing.some((entry) => entry.id === channel.id) || isChannelNameTaken(existing, normalizedName, channel.type)) {
      return state;
    }

    const updatedChannels = [...existing, { ...channel, name: normalizedName }];
    const updatedRoom = { ...state.currentRoom, channels: updatedChannels };

    return {
      ...state,
      currentRoom: updatedRoom,
      savedRooms: upsertRoom(state.savedRooms, updatedRoom),
      activeChannelId: resolveActiveTextChannelId(updatedRoom.channels, state.activeChannelId)
    };
  }),

  on(RoomsActions.removeChannel, (state, { channelId }) => {
    if (!state.currentRoom)
      return state;

    const existing = state.currentRoom.channels || defaultChannels();
    const updatedChannels = existing.filter((channel) => channel.id !== channelId);
    const updatedRoom = { ...state.currentRoom, channels: updatedChannels };

    return {
      ...state,
      currentRoom: updatedRoom,
      savedRooms: upsertRoom(state.savedRooms, updatedRoom),
      activeChannelId: resolveActiveTextChannelId(updatedRoom.channels, state.activeChannelId)
    };
  }),

  on(RoomsActions.renameChannel, (state, { channelId, name }) => {
    if (!state.currentRoom)
      return state;

    const existing = state.currentRoom.channels || defaultChannels();
    const normalizedName = normalizeChannelName(name);
    const existingChannel = existing.find((channel) => channel.id === channelId);

    if (!normalizedName || !existingChannel || isChannelNameTaken(existing, normalizedName, existingChannel.type, channelId)) {
      return state;
    }

    const updatedChannels = existing.map((channel) => (channel.id === channelId ? { ...channel, name: normalizedName } : channel));
    const updatedRoom = { ...state.currentRoom, channels: updatedChannels };

    return {
      ...state,
      currentRoom: updatedRoom,
      savedRooms: upsertRoom(state.savedRooms, updatedRoom)
    };
  })
);
