import clsx from 'clsx';
import { observer } from 'mobx-react-lite';
import {
  ChangeEvent,
  KeyboardEvent,
  MouseEvent,
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { twMerge } from 'tailwind-merge';

import { useStores } from '@/app/(root)/AppProviders';
import { UploadFileConfig } from '@/app/(root)/create/uploaderV2/UploadStateContext';
import SpinnerSVG from '@/components/svg/SpinnerSVG';
import useRecording from '@/hooks/useRecording';
import {
  ArrowUpIcon,
  CheckIcon,
  DiceIcon,
  MicrophoneIcon,
  PlusIcon,
  StopIcon,
  TrashIcon,
} from '@/icons';
import logWebUserEvent from '@/logging/logWebUserEvent';

import { useVoiceRecording } from '../../../create/createV2/useVoiceRecording';
import {
  OptionConfig,
  PresetSuggestion,
  useChatInputStore,
  useChatMessagesStore,
  useChatStore,
} from '../../stores';
import { usePresetsData } from '../../usePresetsData';
import {
  consolidateMessages,
  formatString,
  getOptionValue,
  parsePartialMessage,
} from '../../utils';
import { ActionButton } from './ActionButton';
import { ActionButtonMenuNew } from './ActionButtonMenuNew';
import { BIG_ACTION_BUTTON_GAP, BigActionButton } from './BigActionButton';
import { ChatChoices } from './ChatChoices';
import {
  BUTTON_WIDTH,
  COLLAPSED_HEIGHT,
  ChatInputBarActions,
} from './ChatInputBarActions';
import { ChatInputBarReferences } from './ChatInputBarReferences';
import { ChatInputLoadingText } from './ChatInputLoadingText';
import { ExpandableTextarea } from './ExpandableTextarea';
import { RecordLiveWaveform } from './RecordLiveWaveform';
import { RecordStaticWaveform } from './RecordStaticWaveform';
import { BaseReference, ReferenceType } from './ReferenceTypes';

const CLIP_REFERENCED_CHOICES = [
  'Change genre',
  'Tweak vibe',
  'Like the beginning only',
  'Make it longer',
  'Edit lyrics',
  'Increase quality',
] as const;

const ActionMode = ({
  inputClassName,
  handleTriggerSend,
  onStartTranscribe,
  // onStartRecording,
  // onStartUploadFile,
  referencedClip,
  referencedClipDuration,
  getPlaybackTime,
  onStopTextAnimation,
  onStartTextAnimation,
}: {
  handleTriggerSend: (
    e: KeyboardEvent<HTMLInputElement> | MouseEvent<HTMLButtonElement>
  ) => void;
  onStartTranscribe: () => void;
  //onStartRecording?: () => void;
  //onStartUploadFile?: () => void;
  referencedClip?: any;
  referencedClipDuration?: number;
  getPlaybackTime?: () => number;
  inputClassName?: string;
  onStopTextAnimation?: () => void;
  onStartTextAnimation?: () => void;
}) => {
  const { input, setInput } = useChatInputStore();
  const { isLoading } = useChatMessagesStore();
  const { references, removeReference, isMaximumSessionLength } =
    useChatStore();
  const { msgMap, msgIds, loadedMessages } = useChatMessagesStore();
  const [isComposing, setIsComposing] = useState(false);
  const [isSelected, setIsSelected] = useState(false);
  const inputRef = useRef<HTMLTextAreaElement>(null);
  const lastKeyDownRef = useRef<{ key: string; time: number } | null>(null);
  const lastCompositionEndRef = useRef<number | null>(null);
  const allMessages = useMemo(() => {
    return consolidateMessages([
      ...(loadedMessages || []).map((msg) => ({
        id: msg.message_id,
        data: msg,
      })),
      ...msgIds.map((msgId) => ({ id: msgId, data: msgMap.get(msgId) })),
    ]);
  }, [msgIds, msgMap, loadedMessages]);
  const lastMessage = parsePartialMessage(
    allMessages[allMessages.length - 1]?.data
  );
  const presetsData = usePresetsData();
  const [suggestionIndexes, setSuggestionIndexes] = useState<
    Record<string, number>[] | null
  >(null);
  const [isChanged, setIsChanged] = useState<boolean>(false);
  useEffect(() => {
    if (presetsData?.data) {
      setSuggestionIndexes(
        (presetsData.data.suggestions ?? []).map(
          (suggestion: PresetSuggestion) => {
            return Object.entries(suggestion.options_config ?? {}).reduce(
              (acc, [key, values]) => {
                acc[key] = Math.floor(Math.random() * values.options.length);
                return acc;
              },
              {} as Record<string, number>
            );
          }
        )
      );
    }
  }, [presetsData.data]);
  const randomizeIndexes = useCallback(() => {
    setSuggestionIndexes(
      (presetsData.data?.suggestions ?? []).map(
        (suggestion: PresetSuggestion) => {
          return Object.entries(suggestion.options_config ?? {}).reduce(
            (acc, [key, values]) => {
              acc[key] = Math.floor(Math.random() * values.options.length);
              return acc;
            },
            {} as Record<string, number>
          );
        }
      )
    );
  }, [setSuggestionIndexes, presetsData.data]);

  const isLastMessageAB = useMemo(() => {
    return Boolean(lastMessage?.a) || Boolean(lastMessage?.b);
  }, [lastMessage]);

  const choices =
    lastMessage?.choices ??
    lastMessage?.parsedArguments?.choices ??
    lastMessage?.parsedMessage?.choices ??
    [];
  const hasMessages = useMemo(
    () => msgIds.length > 0 || (loadedMessages?.length ?? 0) > 0,
    [msgIds, loadedMessages]
  );

  // Enhance clip references with actual clip data
  const enhancedReferences = references.map((ref) => {
    if (ref.type === ReferenceType.CLIP && referencedClip) {
      return {
        ...ref,
        title: referencedClip.title || '',
        imageUrl: referencedClip.imageUrl || '',
        duration: referencedClipDuration,
        batchIndex:
          referencedClip.batchIndex !== undefined
            ? referencedClip.batchIndex +
              (referencedClip.metadata?.batchOffset ?? 0)
            : undefined,
        currentTime: getPlaybackTime ? getPlaybackTime() : 0,
      };
    }
    return ref;
  });

  const handleRemoveReference = (referenceId: string) => {
    removeReference(referenceId);
  };

  const handleMouseLeave = useCallback(() => {
    if ((!isChanged || input.trim() === '') && !isSelected) {
      setIsChanged(false);
      onStartTextAnimation?.();
    }
  }, [isChanged, input, isSelected, onStartTextAnimation, setIsChanged]);

  const handleMouseDown = useCallback(
    (e: MouseEvent) => {
      const target = e.target as HTMLElement;
      if (
        target.closest('.input-action-button') !== null ||
        target.closest('button') !== null ||
        target.closest('svg') !== null ||
        target.closest('[role="button"]') !== null ||
        target.closest('[aria-label="Get random prompt"]') !== null
      ) {
        return;
      }
      if ((!isChanged || input.trim() === '') && !isSelected) {
        e.preventDefault();
        inputRef.current?.select();
        setIsSelected(true);
      } else if (isSelected) {
        setIsSelected(false);
      }
    },
    [isChanged, input, isSelected]
  );

  const handleMouseUp = useCallback(
    (e: MouseEvent) => {
      if (isSelected) {
        e.preventDefault();
      }
    },
    [isSelected]
  );

  const handleKeyDown = useCallback(
    async (e: KeyboardEvent<HTMLTextAreaElement>) => {
      const isEnter = (e as any).key === 'Enter' && !e.shiftKey;

      const isActuallyComposing =
        isComposing ||
        (e.nativeEvent as any).isComposing ||
        (lastCompositionEndRef.current &&
          Date.now() - lastCompositionEndRef.current < 50);

      if (isEnter && isActuallyComposing) {
        e.preventDefault();
        return; // Let composition end naturally via onCompositionEnd
      }

      // Guard against double-firing
      const now = Date.now();
      const lastEvent = lastKeyDownRef.current;
      if (lastEvent && lastEvent.key === e.key && now - lastEvent.time < 100) {
        return;
      }
      lastKeyDownRef.current = { key: e.key, time: now };

      // Submit message (not composing)
      if (isEnter && !isLoading) {
        e.preventDefault();
        handleTriggerSend(e as any);
      }
    },
    [isComposing, isLoading, handleTriggerSend, isSelected, setIsSelected]
  );

  const handleBlur = useCallback(() => {
    // Always reset isSelected on blur
    setIsSelected(false);
    if (!isChanged || input.trim() === '') {
      setIsChanged(false); // Reset when animation is triggered
      onStartTextAnimation?.();
    }
  }, [setIsSelected, isChanged, input, onStartTextAnimation, setIsChanged]);

  useEffect(() => {
    if (hasMessages) {
      setInput('');
    }
  }, [hasMessages, setInput]);

  return (
    // eslint-disable-next-line jsx-a11y/no-static-element-interactions
    <div
      className='z-[500] flex flex-col'
      onMouseEnter={onStopTextAnimation}
      onMouseLeave={handleMouseLeave}
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      onBlur={handleBlur}
    >
      {hasMessages && choices.length > 0 ? (
        enhancedReferences.some(
          (ref: BaseReference) => ref.type === ReferenceType.CLIP
        ) ? (
          <ChatChoices choices={[...CLIP_REFERENCED_CHOICES]} />
        ) : (
          <ChatChoices choices={choices} />
        )
      ) : null}
      <div
        className={clsx('rounded-t-[20px] pb-4', {
          'bg-background-primary': hasMessages,
        })}
      >
        <ChatInputBarActions
          plusButton={<ActionButtonMenuNew icon={PlusIcon} />}
          referencesArea={
            enhancedReferences.length > 0 ? (
              <ChatInputBarReferences
                references={enhancedReferences}
                onRemoveReference={handleRemoveReference}
              />
            ) : undefined
          }
          textInputRef={inputRef}
          textInputArea={
            <ExpandableTextarea
              inputClassName={inputClassName}
              inputRef={inputRef}
              value={isLastMessageAB ? '' : input}
              readOnly={isLastMessageAB || isMaximumSessionLength}
              setIsComposing={setIsComposing}
              setIsChanged={setIsChanged}
              setIsSelected={setIsSelected}
              placeholder=' Ask me anything'
              maxHeightPercentage={30}
              onKeyDown={handleKeyDown}
              onChange={async (e: ChangeEvent<HTMLTextAreaElement>) => {
                setInput(e.target.value);
                // Reset isSelected when user types/changes input
                if (isSelected) {
                  setIsSelected(false);
                }
              }}
              className={twMerge(
                'h-full w-full rounded-xl px-1 leading-8 outline-none',
                inputClassName
              )}
              lastCompositionEndRef={lastCompositionEndRef}
              choices={choices}
              onStartTextAnimation={() => {
                setIsSelected(false); // Always reset
                setIsChanged(false);
                onStartTextAnimation?.();
              }}
              onStopTextAnimation={onStopTextAnimation}
            />
          }
          microphoneButton={
            <MicrophoneIcon
              onClick={(e) => {
                onStartTranscribe();
                e.stopPropagation();
              }}
              onMouseDown={(e: MouseEvent) => {
                e.stopPropagation();
              }}
              aria-label='Start voice transcription'
              className='input-action-button m-1 h-6 w-6 cursor-pointer text-foreground-inactive hover:text-foreground-secondary'
            />
          }
          submitButton={
            <ActionButton
              className='input-action-button bg-[url(https://cdn-o.suno.com/auras/pro_aura.jpg)] bg-cover bg-left'
              disabled={isLoading || isLastMessageAB || isMaximumSessionLength}
              icon={ArrowUpIcon}
              onClick={(e) => {
                if (!isLoading && !isComposing) {
                  handleTriggerSend({} as any);
                  e.stopPropagation();
                }
              }}
              onMouseDown={(e: MouseEvent) => {
                e.stopPropagation();
              }}
              type='submit'
              aria-label='Send message'
            />
          }
          diceButton={
            hasMessages || !suggestionIndexes ? undefined : (
              <DiceIcon
                onDoubleClick={(e) => {
                  e.preventDefault();
                  e.stopPropagation();
                }}
                onClick={(e) => {
                  e.stopPropagation();
                  onStopTextAnimation?.();
                  randomizeIndexes();
                  const randomSuggestion = presetsData.data?.suggestions?.[0];
                  if (randomSuggestion) {
                    const randomPrompt = formatString(
                      randomSuggestion.template,
                      Object.entries(
                        randomSuggestion.options_config as Record<
                          string,
                          OptionConfig
                        >
                      ).reduce(
                        (acc, [key, values]) => {
                          const selectedOption =
                            values.options[suggestionIndexes?.[0]?.[key] ?? 0];
                          acc[key] = getOptionValue(
                            selectedOption,
                            values.prefix_with_article
                          );
                          return acc;
                        },
                        {} as Record<string, string>
                      )
                    );
                    setInput(randomPrompt);
                    try {
                      inputRef.current?.focus({ preventScroll: true });
                      // Set cursor to end of text instead of selecting
                      const length = randomPrompt.length;
                      inputRef.current?.setSelectionRange(length, length);
                    } catch {
                      // Fallback for browsers that don't support preventScroll
                      inputRef.current?.focus();
                      const length = randomPrompt.length;
                      inputRef.current?.setSelectionRange(length, length);
                    }
                  }
                }}
                onMouseDown={(e: MouseEvent) => {
                  e.preventDefault();
                  e.stopPropagation();
                }}
                aria-label='Get random prompt'
                className='input-action-button m-1 h-6 w-6 cursor-pointer text-foreground-inactive hover:text-foreground-secondary'
              />
            )
          }
        />
      </div>
    </div>
  );
};

const TranscribeMode = observer(
  ({
    onTranscribe,
    onClose,
  }: {
    onTranscribe: (partialOrFinalText: string) => void;
    onClose: () => void;
  }) => {
    const [waveformData, setWaveformData] = useState<Float32Array | null>(null);
    const { session } = useStores();
    const chatUUID = useChatStore((state) => state.chatUUID);

    /**
     * useVoiceRecording handles mic capture and transcription.
     * We pass a drawWaveform callback to capture the audio data for visualization.
     */
    const {
      startVoiceRecording,
      stopVoiceRecording,
      transcripts,
      currentInterimTranscript,
    } = useVoiceRecording({
      drawWaveform: (channelData: Float32Array) => {
        // Update waveform data for visualization
        setWaveformData(channelData);
      },
    });

    // Start mic on mount, stop on unmount
    // Empty dependency array ensures this only runs once on mount
    useEffect(() => {
      logWebUserEvent(
        {
          actionName: 'OrpheusStartTranscribing',
          context: {
            sessionId: chatUUID,
          },
        },
        session
      );
      startVoiceRecording();
      return () => {
        stopVoiceRecording();
      };
      // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    const fullText = useMemo(() => {
      return (
        transcripts.join(' ') +
        (transcripts.length > 0 && currentInterimTranscript ? ' ' : '') +
        currentInterimTranscript
      );
    }, [transcripts, currentInterimTranscript]);

    // Stream transcription text to parent
    useEffect(() => {
      onTranscribe(fullText.trim());
    }, [fullText, onTranscribe]);

    return (
      <ModeRecorder
        audioData={waveformData || undefined}
        onStop={() => {
          stopVoiceRecording();
          onClose();
          logWebUserEvent(
            {
              actionName: 'OrpheusStopTranscribing',
              context: {
                sessionId: chatUUID,
                transcribedText: fullText.trim(),
              },
            },
            session
          );
        }}
        icon={<CheckIcon className='text-black' />}
        buttonColor='#ffffff'
        waveformColor='#ffffff'
        transcriptionText={fullText.trim()}
      />
    );
  }
);

// Typing animation component
const TypingText = ({ text }: { text: string }) => {
  const [displayedText, setDisplayedText] = useState('');
  const previousTextRef = useRef('');

  useEffect(() => {
    // If text got shorter (e.g., reset), update immediately
    if (text.length < previousTextRef.current.length) {
      setDisplayedText(text);
      previousTextRef.current = text;
      return;
    }

    // Only animate new characters
    if (text.length > displayedText.length) {
      const timeout = setTimeout(() => {
        setDisplayedText(text.slice(0, displayedText.length + 1));
      }, 30); // 30ms per character for smooth typing effect

      return () => clearTimeout(timeout);
    }

    previousTextRef.current = text;
  }, [text, displayedText]);

  return <span>{displayedText}</span>;
};

// Extracted helper components for recording states
const ModeRecorder = ({
  audioData,
  onStop,
  icon,
  buttonColor,
  waveformColor,
  transcriptionText,
}: {
  audioData: Float32Array | null | undefined;
  onStop: () => void;
  icon: React.ReactNode;
  buttonColor: string;
  waveformColor?: string;
  transcriptionText?: string;
}) => {
  return (
    <div className='flex flex-col gap-1 p-2'>
      {/* Transcription text above waveform */}
      <div className='flex min-h-[32px] items-center justify-center px-4'>
        <div className='w-full text-center text-sm leading-snug text-foreground-secondary/50'>
          {transcriptionText ? (
            <TypingText text={transcriptionText} />
          ) : (
            <span className='italic'>Start speaking...</span>
          )}
        </div>
      </div>

      <div
        className='flex'
        style={{
          gap: `${BIG_ACTION_BUTTON_GAP}px`,
          height: `${COLLAPSED_HEIGHT}px`,
        }}
      >
        {/* Column 1: Waveform - expands to available width */}
        <div className='flex flex-1 items-center'>
          <RecordLiveWaveform
            audioData={audioData || undefined}
            isRecording={true}
            barColor={waveformColor}
            glowColor={waveformColor}
          />
        </div>

        {/* Column 2: Action button */}
        <div className='flex items-center justify-center'>
          <BigActionButton
            icon={icon}
            backgroundColor={buttonColor}
            onClick={onStop}
            aria-label='Stop recording'
          />
        </div>
      </div>
    </div>
  );
};

const ModeRecordConfirm = ({
  audioBuffer,
  onTrash,
  onConfirm,
}: {
  audioBuffer: AudioBuffer | null;
  onTrash: () => void;
  onConfirm: () => void;
}) => {
  return (
    <div className={`flex h-[${COLLAPSED_HEIGHT}px] p-2 py-8`}>
      {/* Column 1: Static Waveform - expands to available width */}
      <div className='flex flex-1 items-center justify-center'>
        {audioBuffer ? (
          <RecordStaticWaveform audioBuffer={audioBuffer} className='w-full' />
        ) : null}
      </div>

      {/* Column 2: Loading Spinner - 32px wide, only shown when no audioBuffer */}
      {!audioBuffer && (
        <div
          className={`flex w-[${BUTTON_WIDTH}px] flex-shrink-0 items-center justify-center`}
        >
          <SpinnerSVG className='h-4 w-4 text-foreground-primary/60' />
        </div>
      )}

      {/* Column 3: Trash button - 32px wide */}
      <div
        className={`flex w-[${BUTTON_WIDTH}px] flex-shrink-0 items-center justify-center`}
      >
        <ActionButton
          icon={TrashIcon}
          onClick={onTrash}
          aria-label='Cancel recording'
        />
      </div>

      {/* Column 4: Check button - 32px wide */}
      <div
        className={`flex w-[${BUTTON_WIDTH}px] flex-shrink-0 items-center justify-center`}
      >
        <ActionButton
          icon={CheckIcon}
          onClick={onConfirm}
          aria-label='Confirm recording'
        />
      </div>
    </div>
  );
};

const ModeUploading = ({
  text = 'Uploading recording...',
}: {
  text?: string;
}) => {
  return (
    <div className={`flex h-[${COLLAPSED_HEIGHT}px] py-8`}>
      {/* Column 1: Loading text - expands to available width */}
      <div className='flex flex-1 items-center justify-center'>
        <ChatInputLoadingText text={text} />
      </div>
    </div>
  );
};

const RecordMode = ({
  onStopRecording,
  onDismiss: _onDismiss,
  onUploadComplete: _onUploadComplete,
  onConfirm,
  onClose,
  onRecordStart,
  onRecordEnd,
}: {
  onStopRecording?: (buffer: AudioBuffer | null) => void;
  onDismiss?: () => void;
  onUploadComplete?: (
    clipId: string,
    title: string,
    imageUrl: string,
    uploadId: string
  ) => void;
  onConfirm?: (buffer: AudioBuffer | null) => void;
  onClose?: () => void;
  onRecordStart?: () => void;
  onRecordEnd?: () => void;
}) => {
  const recordingContext = useRecording();
  const [recordedBuffer, setRecordedBuffer] = useState<AudioBuffer | null>(
    null
  );

  // Internal record mode states
  enum RecordModeState {
    RECORDING = 'recording',
    RECORDED = 'recorded',
    UPLOADING = 'uploading',
  }

  const [recordModeState, setRecordModeState] = useState<RecordModeState>(
    RecordModeState.RECORDING
  );

  // Start recording when component mounts
  useEffect(() => {
    console.log(
      'RecordMode useEffect: starting recording, current stage:',
      recordingContext.stage
    );
    recordingContext.startRecording();
    onRecordStart?.();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const handleStopRecording = async () => {
    // Prevent multiple calls by checking current state
    if (
      recordModeState !== RecordModeState.RECORDING ||
      recordingContext.stage !== 'Recording'
    ) {
      console.log(
        'handleStopRecording called but not in recording state, ignoring'
      );
      return;
    }

    console.log(
      'handleStopRecording called, current stage:',
      recordingContext.stage
    );

    // Immediately update state to prevent re-entry
    setRecordModeState(RecordModeState.RECORDED);

    // Call onRecordEnd immediately to remove red border
    onRecordEnd?.();

    try {
      // Add timeout to prevent hanging indefinitely
      const buffer = await Promise.race([
        recordingContext.stopRecording(),
        new Promise<AudioBuffer | null>((_, reject) =>
          setTimeout(() => reject(new Error('Recording stop timeout')), 5000)
        ),
      ]);
      console.log('stopRecording returned buffer:', buffer);
      setRecordedBuffer(buffer);
      onStopRecording?.(buffer);
    } catch (error) {
      console.error('Error stopping recording:', error);
      // Set null buffer so UI shows spinner briefly then user can retry
      setRecordedBuffer(null);
      onStopRecording?.(null);
    }

    // Skip automatic upload - let user decide what to do
  };

  const handleConfirm = async () => {
    if (recordedBuffer) {
      setRecordModeState(RecordModeState.UPLOADING);
      try {
        await onConfirm?.(recordedBuffer);
        // After upload completes successfully, close the record mode
        onClose?.();
      } catch (error) {
        console.error('Upload failed in RecordMode:', error);
        // On error, switch back to action mode (toast already shown by onConfirm)
        onClose?.();
      }
    }
  };

  // Render based on internal state
  const renderRecordModeContent = () => {
    switch (recordModeState) {
      case RecordModeState.RECORDING:
        return (
          <ModeRecorder
            audioData={recordingContext.visualizedWindow}
            onStop={
              recordModeState === RecordModeState.RECORDING
                ? handleStopRecording
                : () => {}
            }
            icon={<StopIcon />}
            buttonColor='#f8441c'
            waveformColor='#f8441c'
          />
        );

      case RecordModeState.RECORDED:
        return (
          <ModeRecordConfirm
            audioBuffer={recordedBuffer}
            onTrash={() => {
              // Just close the record mode, don't handle the audio
              onClose?.();
            }}
            onConfirm={handleConfirm}
          />
        );

      case RecordModeState.UPLOADING:
        return <ModeUploading text='Uploading recording...' />;

      default:
        return null;
    }
  };

  return renderRecordModeContent();
};

const UploadFileMode = ({}: {
  onUploadComplete?: (
    clipId: string,
    title: string,
    imageUrl: string,
    uploadId: string,
    audioBuffer?: AudioBuffer
  ) => void;
  onClose?: () => void;
  uploadFileConfig?: UploadFileConfig;
}) => {
  const renderUploadFileContent = () => {
    return <ModeUploading text='Uploading file...' />;
  };

  return renderUploadFileContent();
};

export const ChatInputModes = {
  ActionMode,
  TranscribeMode,
  RecordMode,
  UploadFileMode,
  // Exported helper components for reuse
  ModeRecorder,
  ModeRecordConfirm,
  ModeUploading,
};
