import { createSlice, createAsyncThunk, createSelector } from '@reduxjs/toolkit';
import { getChatMessages, sendChatMessage, uploadMusicNotation } from '../../helpers/rest';
import { saveLastReadAt, loadLastReadAt } from '../../helpers/chatStorage';

/**
 * Maximum messages retained per channel to prevent unbounded memory growth
 * Oldest messages are removed when this limit is exceeded
 */
const MAX_MESSAGES = 500;

/**
 * Async thunk to fetch chat history for a channel
 * @param {Object} params - Request parameters
 * @param {string} params.channel - Channel ID (session ID, lesson ID, or 'global')
 * @param {string} params.sessionId - Session ID for session channels
 * @param {number} [params.before] - Pagination cursor
 */
export const fetchChatHistory = createAsyncThunk(
  'sessionChat/fetchChatHistory',
  async ({ channel, sessionId, before }) => {
    const response = await getChatMessages({
      channel: sessionId ? 'session' : 'global',
      sessionId,
      before
    });
    return { channel, ...response };
  }
);

/**
 * Async thunk to send a chat message with optimistic UI updates
 * @param {Object} params - Request parameters
 * @param {string} params.channel - Channel ID (session ID, lesson ID, or 'global')
 * @param {string} params.sessionId - Session ID for session channels
 * @param {string} params.message - Message content
 * @param {string} params.optimisticId - Temporary ID for optimistic update
 * @param {string} params.userId - Current user ID
 * @param {string} params.userName - Current user name
 * @param {string} [params.clientId] - WebSocket client ID (used to exclude sender from broadcast)
 */
export const sendMessage = createAsyncThunk(
  'sessionChat/sendMessage',
  async ({ channel, sessionId, message, clientId }) => {
    const response = await sendChatMessage({
      channel: sessionId ? 'session' : 'global',
      sessionId,
      message,
      clientId
    });
    return response;
  }
);

/**
 * Async thunk to upload file attachment to session
 * Creates MusicNotation record, which triggers ChatMessage creation
 * ChatMessage broadcast via WebSocket (handled separately)
 *
 * @param {Object} params
 * @param {File} params.file - File object from input
 * @param {string} params.sessionId - Music session ID
 * @param {string} [params.clientId] - WebSocket client ID (for deduplication)
 */
export const uploadAttachment = createAsyncThunk(
  'sessionChat/uploadAttachment',
  async ({ file, sessionId, clientId, userId, userName }, { rejectWithValue }) => {
    try {
      // Build FormData
      const formData = new FormData();
      formData.append('files[]', file);
      formData.append('session_id', sessionId);

      // Determine attachment type based on file extension
      const ext = file.name.split('.').pop().toLowerCase();
      const audioExts = ['mp3', 'wav', 'flac', 'ogg', 'aiff', 'aifc', 'au'];
      const attachmentType = audioExts.includes(ext) ? 'audio' : 'notation';
      formData.append('attachment_type', attachmentType);

      // Upload via REST API
      const response = await uploadMusicNotation(formData);

      // Return response with user info for optimistic update
      // (sender is excluded from WebSocket broadcast by backend)
      return {
        musicNotations: response,
        sessionId,
        userId,
        userName,
        attachmentType
      };
    } catch (error) {
      // Check for specific error types
      if (error.status === 413) {
        return rejectWithValue('File too large - maximum 10 MB');
      }
      if (error.status === 422) {
        return rejectWithValue('Invalid file type or format');
      }
      return rejectWithValue(error.message || 'Upload failed. Please try again.');
    }
  }
);

/**
 * Initial state for session chat
 * @type {Object}
 * @property {Object.<string, Array>} messagesByChannel - Messages organized by channel ID
 * @property {string|null} activeChannel - Currently active channel ID
 * @property {string|null} channelType - Type of active channel ('global', 'session', 'lesson')
 * @property {Object.<string, number>} unreadCounts - Unread message count per channel
 * @property {Object.<string, string|null>} lastReadAt - ISO timestamp of last read per channel
 * @property {Object.<string, string>} fetchStatus - Fetch status per channel ('idle', 'loading', 'succeeded', 'failed')
 * @property {Object.<string, string|null>} fetchError - Fetch error message per channel
 * @property {string} sendStatus - Send status ('idle', 'loading', 'succeeded', 'failed')
 * @property {string|null} sendError - Send error message
 * @property {Object.<string, number|null>} nextCursors - Pagination cursors per channel
 * @property {boolean} isWindowOpen - Whether chat window is open
 * @property {Object|null} windowPosition - Window position {x, y}
 * @property {Object} uploadState - File upload state tracking
 * @property {string} uploadState.status - Upload status ('idle', 'uploading', 'error')
 * @property {number} uploadState.progress - Upload progress 0-100
 * @property {string|null} uploadState.error - Upload error message
 * @property {string|null} uploadState.fileName - Currently uploading file name
 */
const initialState = {
  messagesByChannel: {},
  activeChannel: null,
  channelType: null,
  unreadCounts: {},
  lastReadAt: loadLastReadAt(), // Load from localStorage on init
  fetchStatus: {},
  fetchError: {},
  sendStatus: 'idle',
  sendError: null,
  nextCursors: {},
  isWindowOpen: false,
  windowPosition: null,
  uploadState: {
    status: 'idle',
    progress: 0,
    error: null,
    fileName: null
  }
};

/**
 * Helper function to get channel key from message
 * Session messages use 'session-{sessionId}', global uses 'global'
 * @param {Object} message - Message object
 * @returns {string} Channel key
 */
const getChannelKey = (message) => {
  if (message.channel === 'session' && message.sessionId) {
    return message.sessionId;
  }
  if (message.channel === 'lesson' && message.lessonSessionId) {
    return message.lessonSessionId;
  }
  return message.channel; // 'global'
};

/**
 * Helper function to create an optimistic message
 * @param {Object} params - Message parameters
 * @returns {Object} Optimistic message object
 */
const createOptimisticMessage = ({ optimisticId, userId, userName, message, channel }) => ({
  id: optimisticId,
  senderId: userId,
  senderName: userName,
  message,
  createdAt: new Date().toISOString(),
  channel,
  isOptimistic: true
});

const sessionChatSlice = createSlice({
  name: 'sessionChat',
  initialState,
  reducers: {
    /**
     * Add message from WebSocket
     * Handles deduplication, sorting, and unread count increment
     */
    addMessageFromWebSocket: (state, action) => {
      const message = action.payload;
      const channel = getChannelKey(message);

      // Initialize channel if not exists
      if (!state.messagesByChannel[channel]) {
        state.messagesByChannel[channel] = [];
      }

      // Deduplicate by msg_id and attachmentId (for attachment messages)
      const existsById = state.messagesByChannel[channel].some(m => m.id === message.id);
      if (existsById) return;

      // For attachment messages, also check by attachmentId to avoid duplicates
      // from optimistic updates that use different ID format
      if (message.attachmentId) {
        const existsByAttachmentId = state.messagesByChannel[channel].some(
          m => m.attachmentId && m.attachmentId === message.attachmentId
        );
        if (existsByAttachmentId) return;
      }

      // Add message
      state.messagesByChannel[channel].push(message);

      // Sort by createdAt ASC
      state.messagesByChannel[channel].sort((a, b) =>
        new Date(a.createdAt) - new Date(b.createdAt)
      );

      // Limit messages to MAX_MESSAGES to prevent unbounded memory growth
      state.messagesByChannel[channel] = state.messagesByChannel[channel].slice(-MAX_MESSAGES);

      // Increment unread count if window closed OR viewing different channel
      if (!state.isWindowOpen || state.activeChannel !== channel) {
        state.unreadCounts[channel] = (state.unreadCounts[channel] || 0) + 1;
      }
    },

    /**
     * Set active channel for viewing
     */
    setActiveChannel: (state, action) => {
      const { channel, channelType } = action.payload;
      state.activeChannel = channel;
      state.channelType = channelType;
    },

    /**
     * Open chat window
     * Resets unread count for active channel and persists to localStorage
     */
    openChatWindow: (state) => {
      state.isWindowOpen = true;

      // Reset unread count for active channel
      if (state.activeChannel) {
        const now = new Date().toISOString();
        state.unreadCounts[state.activeChannel] = 0;
        state.lastReadAt[state.activeChannel] = now;
        saveLastReadAt(state.activeChannel, now); // Persist to localStorage
      }
    },

    /**
     * Close chat window
     * Does not affect unread counts
     */
    closeChatWindow: (state) => {
      state.isWindowOpen = false;
    },

    /**
     * Mark channel as read
     * Resets unread count, updates lastReadAt timestamp, and persists to localStorage
     */
    markAsRead: (state, action) => {
      const { channel } = action.payload;
      const now = new Date().toISOString();
      state.unreadCounts[channel] = 0;
      state.lastReadAt[channel] = now;
      saveLastReadAt(channel, now); // Persist to localStorage
    },

    /**
     * Increment unread count for a channel
     * Initializes count to 1 if channel doesn't exist
     */
    incrementUnreadCount: (state, action) => {
      const { channel } = action.payload;
      state.unreadCounts[channel] = (state.unreadCounts[channel] || 0) + 1;
    },

    /**
     * Set window position for persistence
     * Used by WindowPortal to save drag position
     */
    setWindowPosition: (state, action) => {
      state.windowPosition = action.payload;
    },

    /**
     * Clear send error
     * Used for retry functionality in JKChatComposer
     */
    clearSendError: (state) => {
      state.sendError = null;
      state.sendStatus = 'idle';
    },

    /**
     * Set upload status
     * Updates upload state fields (status, progress, error, fileName)
     */
    setUploadStatus: (state, action) => {
      const { status, progress, error, fileName } = action.payload;
      if (status !== undefined) state.uploadState.status = status;
      if (progress !== undefined) state.uploadState.progress = progress;
      if (error !== undefined) state.uploadState.error = error;
      if (fileName !== undefined) state.uploadState.fileName = fileName;
    },

    /**
     * Clear upload error
     * Resets error to null, status to idle
     */
    clearUploadError: (state) => {
      state.uploadState.error = null;
      state.uploadState.status = 'idle';
    },

    /**
     * Clear all messages when leaving session
     * Called from JKSessionScreen when user leaves to prevent stale data
     */
    clearAllMessages: (state) => {
      state.messagesByChannel = {};
      state.unreadCounts = {};
      state.fetchStatus = {};
      state.fetchError = {};
      state.nextCursors = {};
      // Preserve lastReadAt for persistence across sessions
      // Preserve isWindowOpen and windowPosition for UX continuity
    }
  },
  extraReducers: (builder) => {
    builder
      // fetchChatHistory pending
      .addCase(fetchChatHistory.pending, (state, action) => {
        const channel = action.meta.arg.channel;
        state.fetchStatus[channel] = 'loading';
        state.fetchError[channel] = null;
      })
      // fetchChatHistory fulfilled
      .addCase(fetchChatHistory.fulfilled, (state, action) => {
        const channel = action.meta.arg.channel;
        // API returns { chats: [...], next: ... }
        const { chats, next } = action.payload;
        const messages = chats || [];

        // Initialize channel if not exists
        if (!state.messagesByChannel[channel]) {
          state.messagesByChannel[channel] = [];
        }

        // Transform API response format to internal format
        // API returns: { user: { name: "..." }, user_id: "...", music_notation: { id, file_name, ... } }
        // Internal format: { senderName: "...", senderId: "...", attachmentId: "...", ... }
        // This matches the WebSocket message format (see JKSessionScreen.js handleChatMessage)
        const transformedMessages = messages.map(m => ({
          id: m.id,
          senderId: m.user_id,
          senderName: m.user?.name || 'Unknown',
          message: m.message,
          createdAt: m.created_at,
          channel: m.channel,
          // Attachment fields from REST API (music_notation nested object)
          // Flatten to match WebSocket format for consistent rendering in JKChatMessage
          purpose: m.purpose,
          attachmentId: m.music_notation?.id,
          attachmentName: m.music_notation?.file_name,
          attachmentType: m.music_notation?.attachment_type,
          attachmentSize: null  // Not available in REST API response (only in WebSocket)
        }));

        // Deduplicate messages by ID and by attachmentId (for attachment messages)
        // This handles the case where optimistic attachment message uses 'attachment-{id}' format
        // but the REST API returns the chat message ID
        const existingIds = new Set(state.messagesByChannel[channel].map(m => m.id));
        const existingAttachmentIds = new Set(
          state.messagesByChannel[channel]
            .filter(m => m.attachmentId)
            .map(m => m.attachmentId)
        );
        const newMessages = transformedMessages.filter(m => {
          // Check by message ID first
          if (existingIds.has(m.id)) return false;
          // For attachment messages, also check by attachmentId to avoid duplicates
          // from optimistic updates that use different ID format
          if (m.attachmentId && existingAttachmentIds.has(m.attachmentId)) return false;
          return true;
        });

        // Prepend new messages (oldest first for pagination)
        state.messagesByChannel[channel] = [...newMessages, ...state.messagesByChannel[channel]];

        // Sort by createdAt ASC to maintain chronological order
        state.messagesByChannel[channel].sort((a, b) =>
          new Date(a.createdAt) - new Date(b.createdAt)
        );

        // Limit messages to MAX_MESSAGES to prevent unbounded memory growth
        state.messagesByChannel[channel] = state.messagesByChannel[channel].slice(-MAX_MESSAGES);

        state.fetchStatus[channel] = 'succeeded';
        state.nextCursors[channel] = next;

        // Calculate unread count based on lastReadAt (for page reload persistence)
        // Only set unread count if chat window is NOT open for this channel
        // (if window is open, user is viewing messages so they're not "unread")
        if (!state.isWindowOpen || state.activeChannel !== channel) {
          const lastRead = state.lastReadAt[channel];
          if (lastRead) {
            // Count messages newer than lastReadAt
            const lastReadDate = new Date(lastRead);
            const unreadCount = state.messagesByChannel[channel].filter(
              m => new Date(m.createdAt) > lastReadDate
            ).length;
            state.unreadCounts[channel] = unreadCount;
          } else {
            // No lastReadAt means user never opened chat - all messages are unread
            state.unreadCounts[channel] = state.messagesByChannel[channel].length;
          }
        }
      })
      // fetchChatHistory rejected
      .addCase(fetchChatHistory.rejected, (state, action) => {
        const channel = action.meta.arg.channel;
        state.fetchStatus[channel] = 'failed';
        state.fetchError[channel] = action.error.message;
      })
      // sendMessage pending - optimistic update
      .addCase(sendMessage.pending, (state, action) => {
        state.sendStatus = 'loading';
        state.sendError = null;

        // Optimistic update: add message immediately
        const { channel, message, optimisticId, userId, userName } = action.meta.arg;
        if (!state.messagesByChannel[channel]) {
          state.messagesByChannel[channel] = [];
        }

        const optimisticMessage = createOptimisticMessage({
          optimisticId,
          userId,
          userName,
          message,
          channel
        });
        state.messagesByChannel[channel].push(optimisticMessage);
      })
      // sendMessage fulfilled - replace optimistic message
      .addCase(sendMessage.fulfilled, (state, action) => {
        state.sendStatus = 'succeeded';
        state.sendError = null;

        // Replace optimistic message with real one
        const { channel, optimisticId } = action.meta.arg;
        const realMessage = action.payload; // API returns message directly, not wrapped

        const messages = state.messagesByChannel[channel];
        if (messages) {
          const index = messages.findIndex(m => m.id === optimisticId);
          if (index !== -1) {
            messages[index] = {
              id: realMessage.id,
              senderId: realMessage.user_id,
              senderName: realMessage.user?.name || 'Unknown',
              message: realMessage.message,
              createdAt: realMessage.created_at,
              channel: realMessage.channel
            };
          }
        }
      })
      // sendMessage rejected - remove optimistic message
      .addCase(sendMessage.rejected, (state, action) => {
        state.sendStatus = 'failed';
        state.sendError = action.error.message;

        // Remove optimistic message on failure
        const { channel, optimisticId } = action.meta.arg;
        const messages = state.messagesByChannel[channel];
        if (messages) {
          const index = messages.findIndex(m => m.id === optimisticId);
          if (index !== -1) {
            messages.splice(index, 1);
          }
        }
      })
      // uploadAttachment pending
      .addCase(uploadAttachment.pending, (state, action) => {
        state.uploadState.status = 'uploading';
        state.uploadState.error = null;
        state.uploadState.fileName = action.meta.arg.file.name;
        state.uploadState.progress = 0;
      })
      // uploadAttachment fulfilled
      .addCase(uploadAttachment.fulfilled, (state, action) => {
        state.uploadState.status = 'idle';
        state.uploadState.fileName = null;
        state.uploadState.progress = 0;

        // Add attachment message locally for the uploader
        // (sender is excluded from WebSocket broadcast by backend's server_publish_to_session)
        const { musicNotations, sessionId, userId, userName, attachmentType } = action.payload;

        if (musicNotations && musicNotations.length > 0 && sessionId) {
          const notation = musicNotations[0]; // Single file upload
          const channel = sessionId; // Session channel uses sessionId as key

          // Initialize channel if not exists
          if (!state.messagesByChannel[channel]) {
            state.messagesByChannel[channel] = [];
          }

          // Construct attachment message from upload response
          // Use music_notation.id as part of message id to avoid collision with real msg_id
          const messageId = `attachment-${notation.id}`;

          // Check for duplicates by both message ID AND attachmentId
          // WebSocket may deliver message before this handler runs with different ID format
          const existsById = state.messagesByChannel[channel].some(m => m.id === messageId);
          const existsByAttachmentId = state.messagesByChannel[channel].some(
            m => m.attachmentId && m.attachmentId === notation.id
          );
          if (!existsById && !existsByAttachmentId) {
            const purpose = attachmentType === 'audio' ? 'Audio File' : 'Notation File';
            const message = {
              id: messageId,
              senderId: userId,
              senderName: userName || 'You',
              message: '',
              createdAt: notation.created_at || new Date().toISOString(),
              channel: 'session',
              sessionId: sessionId,
              purpose: purpose,
              attachmentId: notation.id,
              attachmentType: attachmentType,
              attachmentName: notation.file_name,
              attachmentSize: notation.size || null
            };

            state.messagesByChannel[channel].push(message);

            // Sort by createdAt ASC
            state.messagesByChannel[channel].sort((a, b) =>
              new Date(a.createdAt) - new Date(b.createdAt)
            );

            // Limit messages to MAX_MESSAGES to prevent unbounded memory growth
            state.messagesByChannel[channel] = state.messagesByChannel[channel].slice(-MAX_MESSAGES);
          }
        }
      })
      // uploadAttachment rejected
      .addCase(uploadAttachment.rejected, (state, action) => {
        state.uploadState.status = 'error';
        state.uploadState.error = action.error.message || 'Upload failed';
        state.uploadState.progress = 0;
      });
  }
});

export const {
  addMessageFromWebSocket,
  setActiveChannel,
  openChatWindow,
  closeChatWindow,
  markAsRead,
  incrementUnreadCount,
  setWindowPosition,
  clearSendError,
  setUploadStatus,
  clearUploadError,
  clearAllMessages
} = sessionChatSlice.actions;

// Phase 7 Plan 3: Memoized selectors using createSelector from Redux Toolkit

/**
 * Input selectors for memoized selectors
 * These directly access state and are used as inputs to createSelector
 */

/** @private */
const selectSessionChatState = (state) => state.sessionChat;

/** @private */
const selectMessagesByChannel = (state) => state.sessionChat.messagesByChannel;

/** @private */
const selectUnreadCounts = (state) => state.sessionChat.unreadCounts;

/** @private */
const selectFetchStatusMap = (state) => state.sessionChat.fetchStatus;

/**
 * Select messages for a specific channel
 *
 * Returns array of messages for the given channel, or empty array if channel doesn't exist.
 * Memoized with createSelector - only recomputes when messagesByChannel or channel changes.
 *
 * @param {Object} state - Redux state
 * @param {string} channel - Channel ID (session ID, lesson ID, or 'global')
 * @returns {Array} Messages for the channel, sorted chronologically
 */
export const selectChatMessages = createSelector(
  [selectMessagesByChannel, (state, channel) => channel],
  (messagesByChannel, channel) => messagesByChannel[channel] || []
);

/**
 * Select unread count for a specific channel
 *
 * Returns unread count for the given channel, or 0 if channel doesn't exist.
 * Memoized with createSelector - only recomputes when unreadCounts or channel changes.
 *
 * @param {Object} state - Redux state
 * @param {string} channel - Channel ID (session ID, lesson ID, or 'global')
 * @returns {number} Unread count for the channel (0 if none)
 */
export const selectUnreadCount = createSelector(
  [selectUnreadCounts, (state, channel) => channel],
  (unreadCounts, channel) => unreadCounts[channel] || 0
);

/**
 * Select total unread count across all channels
 *
 * Sums unread counts from all channels for badge display.
 * Memoized with createSelector - only recomputes when unreadCounts changes.
 *
 * @param {Object} state - Redux state
 * @returns {number} Total unread count across all channels
 */
export const selectTotalUnreadCount = createSelector(
  [selectUnreadCounts],
  (unreadCounts) => Object.values(unreadCounts).reduce((sum, count) => sum + count, 0)
);

/**
 * Select whether chat window is currently open
 *
 * Used to control chat window visibility and unread increment logic.
 * Memoized with createSelector - only recomputes when sessionChat state changes.
 *
 * @param {Object} state - Redux state
 * @returns {boolean} True if chat window is open
 */
export const selectIsChatWindowOpen = createSelector(
  [selectSessionChatState],
  (chatState) => chatState.isWindowOpen
);

/**
 * Select currently active channel ID
 *
 * Identifies which channel is currently being viewed in the chat window.
 * Memoized with createSelector - only recomputes when sessionChat state changes.
 *
 * @param {Object} state - Redux state
 * @returns {string|null} Active channel ID, or null if none selected
 */
export const selectActiveChannel = createSelector(
  [selectSessionChatState],
  (chatState) => chatState.activeChannel
);

/**
 * Select fetch status for a specific channel
 *
 * Returns loading state for message history fetch operations.
 * Defaults to 'idle' if channel has never been fetched.
 * Memoized with createSelector - only recomputes when fetchStatus or channel changes.
 *
 * @param {Object} state - Redux state
 * @param {string} channel - Channel ID
 * @returns {string} Fetch status: 'idle' | 'loading' | 'succeeded' | 'failed'
 */
export const selectFetchStatus = createSelector(
  [selectFetchStatusMap, (state, channel) => channel],
  (fetchStatus, channel) => fetchStatus[channel] || 'idle'
);

/**
 * Select send message status
 *
 * Returns loading state for the current message send operation.
 * Memoized with createSelector - only recomputes when sessionChat state changes.
 *
 * @param {Object} state - Redux state
 * @returns {string} Send status: 'idle' | 'loading' | 'succeeded' | 'failed'
 */
export const selectSendStatus = createSelector(
  [selectSessionChatState],
  (chatState) => chatState.sendStatus
);

/**
 * Select send error message
 *
 * Returns error message if the last send operation failed.
 * Memoized with createSelector - only recomputes when sessionChat state changes.
 *
 * @param {Object} state - Redux state
 * @returns {string|null} Error message, or null if no error
 */
export const selectSendError = createSelector(
  [selectSessionChatState],
  (chatState) => chatState.sendError
);

/**
 * Select upload status
 *
 * Returns current upload status: 'idle', 'uploading', or 'error'.
 *
 * @param {Object} state - Redux state
 * @returns {string} Upload status
 */
export const selectUploadStatus = (state) => state.sessionChat.uploadState.status;

/**
 * Select upload error message
 *
 * Returns error message if upload failed.
 *
 * @param {Object} state - Redux state
 * @returns {string|null} Error message, or null if no error
 */
export const selectUploadError = (state) => state.sessionChat.uploadState.error;

/**
 * Select upload progress
 *
 * Returns upload progress 0-100.
 *
 * @param {Object} state - Redux state
 * @returns {number} Upload progress percentage
 */
export const selectUploadProgress = (state) => state.sessionChat.uploadState.progress;

/**
 * Select upload file name
 *
 * Returns currently uploading file name.
 *
 * @param {Object} state - Redux state
 * @returns {string|null} File name, or null if not uploading
 */
export const selectUploadFileName = (state) => state.sessionChat.uploadState.fileName;

/**
 * Select whether currently uploading
 *
 * Returns true if status is 'uploading'.
 *
 * @param {Object} state - Redux state
 * @returns {boolean} True if uploading
 */
export const selectIsUploading = (state) => state.sessionChat.uploadState.status === 'uploading';

export default sessionChatSlice.reducer;
