import React, {
  useCallback,
  useEffect,
  useImperativeHandle,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from 'react';
import { useFormField } from '../../hooks/form';
import { useDropZone } from '../../hooks/use-drop-zone';
import {
  SilkeTextFieldOutline,
  SilkeTextFieldOutlineProps,
} from '../silke-text-field/silke-text-field-outline';
import { AttachmentList, AttachmentListHandle } from './attachment-list';
import {
  SilkeFileCategory,
  fileToAttachmentValue,
  getFileFromClipboard,
  getFileFromDragEvent,
  resolveFileCategories,
} from './file-utils';
import { parseTextToSilkeCommands } from './silke-command-parser';
import styles from './silke-command-text-field.module.scss';
import {
  KEY_DIRECTION,
  SilkeCommand,
  SilkeCommandTextFieldValue,
  SilkeFileValue,
  SilkeImageValue,
  convertSilkeCommandsToString,
  filterAndSortCommands,
  getActiveCommandIndex,
  getCaretOffset,
  getItemLength,
  getTextLength,
  hasSelectionLength,
  isAttachment,
  isSilkeCommand,
  isText,
  setCaretOffset,
  valueToHtml,
} from './utils';

export type SilkeCommandTextFieldProps = SilkeTextFieldOutlineProps & {
  /**
   * File type categories to accept as attachments.
   * - `'image'` — all image types (png, jpeg, gif, etc.)
   * - `'text'` — documents and text files (PDF, Word, Excel, CSV, TXT, JSON, etc.)
   * Defaults to `[]` (no files accepted).
   */
  acceptedFileTypes?: SilkeFileCategory[];
  autoFocus?: boolean;
  availableCommands: SilkeCommand[];
  disabled?: boolean;
  /** Maximum height in pixels before the field starts scrolling */
  maxHeight?: number;
  name?: string;
  onChange: (value: SilkeCommandTextFieldValue[]) => void;
  onCommandFocus?: (index: number, command: SilkeCommandTextFieldValue | undefined) => void;
  onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
  onSuggestions?: (suggestions: SilkeCommand[]) => void;
  placeholder?: string;
  value: SilkeCommandTextFieldValue[];
};

type ActiveCommandState = {
  index: number;
  localOffset: number;
};

export type SilkeCommandTextFieldHandle = {
  /** Remove focus from the input */
  blur: () => void;
  /** Focus the input */
  focus: () => void;
  /** Open the file picker dialog (requires acceptedFileTypes to be set) */
  openFilePicker: () => void;
  /** Get the current value as a string */
  toString: (value?: SilkeCommandTextFieldValue[]) => string;
  /**
   * Update command in the commands array.
   * @param command - the command to update
   * @param index - defaults to active index, use -1 to append
   */
  update: (command: SilkeCommand, index?: number) => void;
};

const SilkeCommandTextField = React.forwardRef<
  SilkeCommandTextFieldHandle,
  SilkeCommandTextFieldProps
>((props, ref) => {
  const {
    acceptedFileTypes,
    autoFocus = false,
    availableCommands,
    disabled = false,
    maxHeight,
    onChange,
    onCommandFocus,
    onKeyDown,
    onSuggestions,
    placeholder,
    value,
    children,
    ...rest
  } = useFormField(props);

  const inputRef = useRef<HTMLDivElement>(null);
  const attachmentListRef = useRef<AttachmentListHandle>(null);
  const valueRef = useRef<SilkeCommandTextFieldValue[]>(value);
  valueRef.current = value;

  // Resolve file categories to MIME patterns
  const effectiveAcceptedTypes = useMemo(
    () => resolveFileCategories(acceptedFileTypes ?? []),
    [acceptedFileTypes],
  );

  const filesDisabled = effectiveAcceptedTypes.length === 0;

  // Extract attachments (images + files) and text values
  const attachments = useMemo(() => value.filter(isAttachment), [value]);
  // Get non-attachment values for text field rendering
  const textValues = useMemo(() => value.filter((v) => !isAttachment(v)), [value]);

  const [activeCommand, setActiveCommand] = useState<ActiveCommandState>(() => {
    const lastIndex = Math.max(0, value.length - 1);
    const lastItem = value[lastIndex];
    return {
      index: lastIndex,
      localOffset: lastItem ? getItemLength(lastItem) : 0,
    };
  });

  const html = useMemo(() => valueToHtml(textValues || [], styles), [textValues]);
  const isEmpty =
    !textValues ||
    textValues.length === 0 ||
    convertSilkeCommandsToString(textValues).trim().length === 0;

  // Restore caret position after value changes
  useLayoutEffect(() => {
    const element = inputRef.current;
    if (!element || document.activeElement !== element) return;

    // Don't collapse an active text selection (e.g. from double-click word selection)
    if (hasSelectionLength()) return;

    const calculatedCaretPos =
      getTextLength(value, activeCommand.index) + activeCommand.localOffset;
    setCaretOffset(element, calculatedCaretPos);
  }, [activeCommand.index, activeCommand.localOffset, value]);

  const updateValue = useCallback(
    (command: SilkeCommand, index: number, insert = false) => {
      const newValue = [...valueRef.current];

      if (index === -1) {
        newValue.push(command);
        index = newValue.length - 1;
      } else if (insert) {
        // Insert at index without replacing
        newValue.splice(index, 0, command);
      } else {
        newValue[index] = command;
      }

      // Insert space after the command
      newValue.splice(index + 1, 0, { type: 'text', value: ' ' });

      // Move caret after the space
      setActiveCommand({ index: index + 1, localOffset: 1 });
      onChange(newValue);
      inputRef.current?.focus();
    },
    [onChange],
  );

  const focusAtEnd = useCallback(() => {
    const element = inputRef.current;
    if (!element) return;
    element.focus();
    const currentValue = valueRef.current;
    const lastIndex = Math.max(0, currentValue.length - 1);
    const lastItem = currentValue[lastIndex];
    const offset = lastItem ? getItemLength(lastItem) : 0;
    const caretPos = getTextLength(currentValue, lastIndex) + offset;
    setCaretOffset(element, caretPos);
    setActiveCommand({ index: lastIndex, localOffset: offset });
  }, []);

  useImperativeHandle(
    ref,
    () => ({
      blur: () => inputRef.current?.blur(),
      focus: focusAtEnd,
      openFilePicker: () => attachmentListRef.current?.openFilePicker(),
      toString: (val = valueRef.current || []) => convertSilkeCommandsToString(val),
      update: (command, index = activeCommand.index) => {
        // If the current item is a completed command, insert after it instead of replacing
        const currentItem = valueRef.current[index];
        if (isSilkeCommand(currentItem) && !currentItem.partialMatch) {
          updateValue(command, index + 1, true);
        } else {
          updateValue(command, index);
        }
      },
    }),
    [activeCommand.index, focusAtEnd, updateValue],
  );

  // Handle auto focus
  useEffect(() => {
    if (autoFocus && !disabled) {
      focusAtEnd();
    }
  }, [autoFocus, disabled, focusAtEnd]);

  // Handle focus event to position caret at end
  const handleFocus = useCallback(() => {
    const element = inputRef.current;
    if (!element) return;
    // Use requestAnimationFrame to ensure DOM is ready
    requestAnimationFrame(() => {
      const currentValue = valueRef.current;
      const lastIndex = Math.max(0, currentValue.length - 1);
      const lastItem = currentValue[lastIndex];
      const offset = lastItem ? getItemLength(lastItem) : 0;
      const caretPos = getTextLength(currentValue, lastIndex) + offset;
      setCaretOffset(element, caretPos);
      setActiveCommand({ index: lastIndex, localOffset: offset });
    });
  }, []);

  // Notify parent of command focus changes
  useEffect(() => {
    onCommandFocus?.(activeCommand.index, valueRef.current[activeCommand.index]);
  }, [activeCommand.index, onCommandFocus]);

  // Track selection changes to update active command
  useEffect(() => {
    const handleSelectionChange = () => {
      const element = inputRef.current;
      if (!element) return;

      // Only update if selection is within this element
      const { activeElement } = document;
      if (activeElement !== element && !element.contains(activeElement)) return;

      const caretPos = getCaretOffset(element);
      const currentValue = valueRef.current;
      const newActiveIndex = getActiveCommandIndex(caretPos, currentValue);
      let newLocalOffset = caretPos - getTextLength(currentValue, newActiveIndex);

      const item = currentValue[newActiveIndex];

      // Snap caret to boundary if inside a non-partial command
      if (isSilkeCommand(item) && !item.partialMatch) {
        const itemLength = getItemLength(item);
        if (newLocalOffset > 0 && newLocalOffset < itemLength) {
          // Snap to nearest boundary
          newLocalOffset = newLocalOffset < itemLength / 2 ? 0 : itemLength;
          const newCaretPos = getTextLength(currentValue, newActiveIndex) + newLocalOffset;
          setCaretOffset(element, newCaretPos);
        }
      }

      setActiveCommand({ index: newActiveIndex, localOffset: newLocalOffset });
    };

    document.addEventListener('selectionchange', handleSelectionChange);
    return () => document.removeEventListener('selectionchange', handleSelectionChange);
  }, []);

  const updateActiveCommandFromCaret = useCallback(
    (newValue: SilkeCommandTextFieldValue[], caretPos?: number) => {
      const pos = caretPos ?? getCaretOffset(inputRef.current);
      const newActiveIndex = getActiveCommandIndex(pos, newValue);
      const newLocalOffset = pos - getTextLength(newValue, newActiveIndex);
      setActiveCommand({ index: newActiveIndex, localOffset: newLocalOffset });
    },
    [],
  );

  const handleInput = useCallback(
    (e: React.FormEvent<HTMLDivElement>) => {
      // Preserve existing attachments when parsing new text
      const existingAttachments = valueRef.current.filter(isAttachment);
      const parsedValue = parseTextToSilkeCommands(e.currentTarget.innerText, availableCommands);
      const newValue = [...existingAttachments, ...parsedValue];
      valueRef.current = newValue;
      updateActiveCommandFromCaret(newValue);
      onChange(newValue);
    },
    [availableCommands, onChange, updateActiveCommandFromCaret],
  );

  // Update suggestions when active command changes
  useEffect(() => {
    if (!onSuggestions) return;

    const currentCommand = valueRef.current[activeCommand.index];
    if (isSilkeCommand(currentCommand) && currentCommand.partialMatch) {
      // Get commands already used in the value (completed, non-partial commands)
      const usedCommands = new Set(
        valueRef.current
          .filter((item): item is SilkeCommand => isSilkeCommand(item) && !item.partialMatch)
          .map((cmd) => `${cmd.type}:${cmd.value}`),
      );

      const suggestions = filterAndSortCommands(
        availableCommands,
        currentCommand.value,
        currentCommand.type,
      ).filter((cmd) => !usedCommands.has(`${cmd.type}:${cmd.value}`));

      onSuggestions(suggestions);
    } else {
      onSuggestions([]);
    }
  }, [activeCommand.index, activeCommand.localOffset, availableCommands, onSuggestions]);

  const handleArrowNavigation = useCallback(
    (
      e: React.KeyboardEvent,
      direction: number,
      commandIndex: number,
      currentValue: SilkeCommandTextFieldValue[],
    ) => {
      e.preventDefault();

      if (direction > 0) {
        // Moving right: skip to end of command or start of next item
        if (commandIndex >= currentValue.length - 1) {
          const item = currentValue[commandIndex];
          setActiveCommand({ index: commandIndex, localOffset: getItemLength(item) });
        } else {
          setActiveCommand({ index: commandIndex + 1, localOffset: 0 });
        }
      } else {
        // Moving left: skip to start of command or end of previous item
        if (commandIndex <= 0) {
          setActiveCommand({ index: 0, localOffset: 0 });
        } else {
          const prevItem = currentValue[commandIndex - 1];
          setActiveCommand({
            index: commandIndex - 1,
            localOffset: getItemLength(prevItem),
          });
        }
      }
    },
    [],
  );

  const handleDelete = useCallback(
    (
      e: React.KeyboardEvent,
      commandIndex: number,
      currentValue: SilkeCommandTextFieldValue[],
      isForward: boolean,
    ) => {
      e.preventDefault();

      const newValue = [...currentValue];
      newValue.splice(commandIndex, 1);

      if (isForward) {
        // Delete key: stay at current position
        const nextIndex = Math.min(commandIndex, newValue.length - 1);
        setActiveCommand({ index: Math.max(0, nextIndex), localOffset: 0 });
      } else {
        // Backspace: move to end of previous item
        const prevIndex = Math.max(commandIndex - 1, 0);
        const prevItem = newValue[prevIndex];
        setActiveCommand({ index: prevIndex, localOffset: prevItem ? getItemLength(prevItem) : 0 });
      }
      onChange(newValue);
    },
    [onChange],
  );

  const handleCharacterInput = useCallback(
    (
      e: React.KeyboardEvent,
      commandIndex: number,
      commandItem: SilkeCommandTextFieldValue,
      currentValue: SilkeCommandTextFieldValue[],
    ) => {
      const element = inputRef.current;
      if (!element) return;

      // Only handle single character input without modifiers
      if (e.key.length !== 1 || e.ctrlKey || e.metaKey || e.altKey) return;

      const currentCaretPos = getCaretOffset(element);
      const startOfCommand = getTextLength(currentValue, commandIndex);
      const itemLength = getItemLength(commandItem);
      const localOffset = currentCaretPos - startOfCommand;

      e.preventDefault();

      const newValue = [...currentValue];

      if (localOffset <= 0) {
        // At start of command - insert text before
        const prevItem = newValue[commandIndex - 1];
        if (commandIndex > 0 && isText(prevItem)) {
          // Append to previous text segment
          newValue[commandIndex - 1] = {
            type: 'text',
            value: prevItem.value + e.key,
          };
          setActiveCommand({
            index: commandIndex - 1,
            localOffset: prevItem.value.length + 1,
          });
        } else {
          // Insert new text segment before command
          newValue.splice(commandIndex, 0, { type: 'text', value: e.key });
          setActiveCommand({ index: commandIndex, localOffset: 1 });
        }
        onChange(newValue);
      } else if (localOffset >= itemLength) {
        // At end of command - insert text after
        const nextItem = newValue[commandIndex + 1];
        if (commandIndex < newValue.length - 1 && isText(nextItem)) {
          // Prepend to next text segment
          newValue[commandIndex + 1] = {
            type: 'text',
            value: e.key + nextItem.value,
          };
          setActiveCommand({ index: commandIndex + 1, localOffset: 1 });
        } else {
          // Insert new text segment after command
          newValue.splice(commandIndex + 1, 0, { type: 'text', value: e.key });
          setActiveCommand({ index: commandIndex + 1, localOffset: 1 });
        }
        onChange(newValue);
      }
      // If inside command (not at boundary), just prevent - no insertion
    },
    [onChange],
  );

  const handleKeyDown = useCallback(
    (e: React.KeyboardEvent<HTMLDivElement>) => {
      onKeyDown?.(e);

      if (disabled) {
        e.preventDefault();
        return;
      }

      // Cmd+Enter or Shift+Enter inserts a line break
      if (e.key === 'Enter' && (e.metaKey || e.shiftKey)) {
        e.preventDefault();
        document.execCommand('insertLineBreak');
        return;
      }

      const element = inputRef.current;
      if (!element) return;

      const direction = KEY_DIRECTION[e.key] || 0;
      const currentCaretPos = getCaretOffset(element);
      const caretPos = currentCaretPos + direction;
      const currentValue = valueRef.current;
      const commandIndex = getActiveCommandIndex(caretPos, currentValue);
      const commandItem = currentValue[commandIndex];

      // Don't interfere if there's a text selection
      if (hasSelectionLength()) return;

      // Prevent deleting a space that separates two completed commands
      if (e.key === 'Backspace' || e.key === 'Delete') {
        const isBackspace = e.key === 'Backspace';
        // For backspace, check char before caret; for delete, check char at caret
        const deleteTargetPos = isBackspace ? currentCaretPos - 1 : currentCaretPos;
        const targetIndex = getActiveCommandIndex(deleteTargetPos, currentValue);
        const targetItem = currentValue[targetIndex];

        // Check if we're about to delete a space-only segment between two commands
        if (isText(targetItem) && targetItem.value.trim() === '' && targetItem.value.length === 1) {
          const prevItem = currentValue[targetIndex - 1];
          const nextItem = currentValue[targetIndex + 1];
          const hasPrevCommand = isSilkeCommand(prevItem) && !prevItem.partialMatch;
          const hasNextCommand = isSilkeCommand(nextItem) && !nextItem.partialMatch;

          if (hasPrevCommand && hasNextCommand) {
            // Delete the space and the following command instead
            e.preventDefault();
            const newValue = [...currentValue];
            newValue.splice(targetIndex, 2); // Remove space and next command
            const prevItemLen = getItemLength(prevItem);
            setActiveCommand({ index: targetIndex - 1, localOffset: prevItemLen });
            onChange(newValue);
            return;
          }
        }
      }

      // Only handle special keys for complete (non-partial) commands
      if (!isSilkeCommand(commandItem) || commandItem.partialMatch) return;

      // Calculate if target position is strictly inside the command (not at boundaries)
      const startOfCommand = getTextLength(currentValue, commandIndex);
      const itemLength = getItemLength(commandItem);
      const localOffset = caretPos - startOfCommand;
      const isStrictlyInside = localOffset > 0 && localOffset < itemLength;

      switch (e.key) {
        case 'ArrowLeft':
        case 'ArrowRight':
          // Only skip command if we would land strictly inside it
          if (isStrictlyInside) {
            handleArrowNavigation(e, direction, commandIndex, currentValue);
          }
          // If at boundary, allow normal navigation
          break;
        case 'Backspace':
        case 'Delete':
          // Delete entire command if target is inside or we're at appropriate boundary
          if (isStrictlyInside) {
            handleDelete(e, commandIndex, currentValue, e.key === 'Delete');
          }
          // If at boundary (localOffset = 0 or itemLength), let browser handle
          break;
        default:
          handleCharacterInput(e, commandIndex, commandItem, currentValue);
      }
    },
    [disabled, handleArrowNavigation, handleCharacterInput, handleDelete, onKeyDown],
  );

  const addAttachment = useCallback(
    (attachmentValue: SilkeImageValue | SilkeFileValue) => {
      const newValue = [...valueRef.current, attachmentValue];
      onChange(newValue);
      inputRef.current?.focus();
    },
    [onChange],
  );

  const removeAttachment = useCallback(
    (index: number) => {
      // Find the nth attachment in the value array and remove it
      let attachmentCount = 0;
      const newValue = valueRef.current.filter((item) => {
        if (isAttachment(item)) {
          if (attachmentCount === index) {
            attachmentCount++;
            return false;
          }
          attachmentCount++;
        }
        return true;
      });
      onChange(newValue);
    },
    [onChange],
  );

  const addMultipleAttachments = useCallback(
    (newAttachments: (SilkeImageValue | SilkeFileValue)[]) => {
      onChange([...valueRef.current, ...newAttachments]);
      inputRef.current?.focus();
    },
    [onChange],
  );

  const handlePaste = useCallback(
    (e: React.ClipboardEvent<HTMLDivElement>) => {
      if (filesDisabled) return;

      const file = getFileFromClipboard(e, effectiveAcceptedTypes);
      if (!file) return;

      e.preventDefault();
      fileToAttachmentValue(file, effectiveAcceptedTypes).then((attachmentValue) => {
        if (attachmentValue) addAttachment(attachmentValue);
      });
    },
    [addAttachment, effectiveAcceptedTypes, filesDisabled],
  );

  const containerRef = useRef<HTMLDivElement>(null);

  const handleDrop = useCallback(
    (e: DragEvent) => {
      if (filesDisabled) return;

      const file = getFileFromDragEvent(e, effectiveAcceptedTypes);
      if (!file) return;

      fileToAttachmentValue(file, effectiveAcceptedTypes).then((attachmentValue) => {
        if (attachmentValue) addAttachment(attachmentValue);
      });
    },
    [addAttachment, effectiveAcceptedTypes, filesDisabled],
  );

  useDropZone(containerRef, handleDrop);

  return (
    <SilkeTextFieldOutline {...rest}>
      <div
        className={styles.container}
        ref={containerRef}
        style={maxHeight ? { maxHeight, overflowY: 'auto' } : undefined}
      >
        {!filesDisabled && (
          <AttachmentList
            ref={attachmentListRef}
            acceptedTypes={effectiveAcceptedTypes}
            attachments={attachments}
            disabled={disabled}
            onAdd={addMultipleAttachments}
            onRemove={removeAttachment}
          />
        )}
        <div className={styles.textfieldRow}>
          <div
            aria-disabled={disabled}
            aria-placeholder={placeholder}
            className={styles.textfield}
            contentEditable={!disabled}
            dangerouslySetInnerHTML={{ __html: html }}
            onFocus={handleFocus}
            onInput={handleInput}
            onKeyDown={handleKeyDown}
            onPaste={handlePaste}
            ref={inputRef}
            role="textbox"
            suppressContentEditableWarning
            tabIndex={disabled ? -1 : 0}
          />
          {placeholder && isEmpty && <div className={styles.placeholder}>{placeholder}</div>}
          {children}
        </div>
      </div>
    </SilkeTextFieldOutline>
  );
});

SilkeCommandTextField.displayName = 'SilkeCommandTextField';

export { SilkeCommandTextField };
