import {
  getCurrentProject,
  subscribeToProjectChange,
  useChainType,
  useModelKey,
  useProjectLoading,
} from "@/aiParams";
import { ChainType } from "@/chainType";
import { Button } from "@/components/ui/button";
import { ModelSelector, type ModelSelectorEntry } from "@/components/ui/ModelSelector";
import type { CopilotMode } from "@/agentMode";
import { isPlusChain } from "@/utils";
import {
  mergeWebTabContexts,
  normalizeUrlString,
  normalizeWebTabContext,
} from "@/utils/urlNormalization";

import { SelectedTextContext, WebTabContext } from "@/types/message";
import { isAllowedFileForNoteContext } from "@/utils";
import { getFileIdentityKey } from "@/utils/fileListUtils";
import { ArrowUp, CornerDownLeft, Loader2, Square, X } from "lucide-react";
import { App, TFile, TFolder } from "obsidian";
import React, {
  useCallback,
  useEffect,
  useImperativeHandle,
  useMemo,
  useRef,
  useState,
  useSyncExternalStore,
} from "react";
import { $getSelection, $isRangeSelection, LexicalEditor as LexicalEditorType } from "lexical";
import { ContextControl } from "./ContextControl";
import { AddContextButton } from "./AddContextButton";
import { openImagePicker } from "./openImagePicker";
import { shouldShowAtMentionTools } from "./hooks/useAtMentionCategories";
import { ModelEffortPicker } from "@/components/ui/ModelEffortPicker";
import { ModePicker } from "@/components/ui/ModePicker";
import { $removePillsByPath } from "./pills/NotePillNode";
import { $removeActiveNotePills } from "./pills/ActiveNotePillNode";
import { $removePillsByURL } from "./pills/URLPillNode";
import { $removePillsByFolder } from "./pills/FolderPillNode";
import { $removePillsByToolName, $createToolPillNode } from "./pills/ToolPillNode";
import { $removeActiveWebTabPills } from "./pills/ActiveWebTabPillNode";
import { $findWebTabPills, $removeWebTabPillsByUrl } from "./pills/WebTabPillNode";
import LexicalEditor from "./LexicalEditor";
import { cn } from "@/lib/utils";
import { type AgentMentionBrand, EMPTY_AGENT_MENTION_BRANDS } from "./hooks/useAtMentionCategories";
import { EMPTY_CLOUD_AGENT_IDS } from "./context/CloudAgentContext";
import { $createAgentPillNode } from "./pills/AgentPillNode";

const ACCENT_CIRCLE_BUTTON_CLASS =
  "tw-rounded-full tw-bg-interactive-accent tw-text-on-accent hover:tw-bg-interactive-accent-hover";

export interface ChatInputProps {
  /**
   * Accessory rendered in a structural column to the right of the
   * badge/image/editor stack (top-aligned). Being a real layout column — not
   * an overlay — no content needs open-ended avoidance padding; the only
   * deliberate exception is the column's own negative margin at the mount
   * point (a few px of top-corner proximity, documented there). Kept as a
   * single flat slot on purpose; if a second accessory ever appears,
   * consolidate into a config object, not more props.
   */
  topRightAccessory?: React.ReactNode;
  inputMessage: string;
  setInputMessage: (message: string) => void;
  handleSendMessage: (metadata?: {
    toolCalls?: string[];
    urls?: string[];
    contextNotes?: TFile[];
    contextFolders?: string[];
    webTabs?: WebTabContext[];
  }) => void;
  isGenerating: boolean;
  onStopGenerating: () => void;
  app: App;
  contextNotes: TFile[];
  setContextNotes: React.Dispatch<React.SetStateAction<TFile[]>>;
  includeActiveNote: boolean;
  setIncludeActiveNote: (include: boolean) => void;
  includeActiveWebTab: boolean;
  setIncludeActiveWebTab: (include: boolean) => void;
  activeWebTab: WebTabContext | null;
  selectedImages: File[];
  onAddImage: (files: File[]) => void;
  setSelectedImages: React.Dispatch<React.SetStateAction<File[]>>;
  disableModelSwitch?: boolean;
  /**
   * Optional override that swaps the default model picker plumbing
   * (`useModelKey()` + `settings.activeModels`) for a caller-supplied model
   * list, value, and change handler. Used by Agent Mode to surface the
   * agent's reported `availableModels` alongside Copilot-configured ones
   * without `ChatInput` needing to know anything about Agent Mode.
   */
  modelPickerOverride?: {
    models: ModelSelectorEntry[];
    value: string;
    onChange: (modelKey: string) => void;
    disabled?: boolean;
    /**
     * Optional sibling effort picker. Surface only when the active model
     * supports effort (modelId-suffix variants or a SessionConfigOption).
     * `value: null` represents the bare/"Default" variant.
     */
    effort?: {
      options: { label: string; value: string | null }[];
      value: string | null;
      onChange: (value: string | null) => void;
      disabled?: boolean;
    };
    /**
     * Per-entry effort catalog used by the merged model+effort picker. When
     * present, `ChatInput` renders a unified popover; when absent, it falls
     * back to the legacy `ModelSelector`.
     */
    effortOptionsByModelKey?: Record<string, { label: string; value: string | null }[]>;
    /**
     * Atomic `(model, effort)` commit. Same-backend picks push both through
     * `applySelection`; cross-backend picks seed a fresh session on the
     * target with the drafted selection.
     */
    commitSelection?: (modelKey: string, effort: string | null) => void;
  };
  /**
   * Optional operational-mode picker (Agent Mode). Surfaces Copilot-canonical
   * modes (build/plan/auto-build) when the active backend exposes them. The
   * picker hook owns the mapping to native ACP ids; ChatInput just renders.
   * Independent of `modelPickerOverride` because mode and model+effort have
   * no functional overlap.
   */
  modePickerOverride?: {
    options: { label: string; value: CopilotMode }[];
    value: CopilotMode | null;
    onChange: (value: CopilotMode) => void;
    disabled?: boolean;
  };
  selectedTextContexts?: SelectedTextContext[];
  onRemoveSelectedText?: (id: string) => void;
  showProgressCard: () => void;
  showIndexingCard?: () => void;

  /**
   * Render slot for the toggle row that sits next to the send button.
   * Chat mode plugs in `<ChatToolControls />`; agent mode omits it so the
   * autonomous-agent and vault/web/composer toggles never appear.
   */
  toolControls?: React.ReactNode;

  /**
   * Fires whenever the set of tool pills (`@vault`, `@websearch`, `@composer`)
   * inside the editor changes. Lets a wrapper mirror toggle state from pills.
   */
  onToolPillsChange?: (toolNames: string[]) => void;

  /** Fires when the user picks a `#tag` from typeahead. */
  onTagSelected?: () => void;

  /** Optional ESC handler invoked when the editor has focus. */
  onEscape?: () => void;
  /** Optional Shift+Tab handler invoked when the editor has focus. */
  onShiftTab?: () => void;

  // Edit mode props
  editMode?: boolean;
  onEditSave?: (
    text: string,
    context: {
      notes: TFile[];
      urls: string[];
      folders: string[];
    }
  ) => void;
  onEditCancel?: () => void;
  initialContext?: {
    notes?: TFile[];
    urls?: string[];
    folders?: string[];
  };

  /**
   * Set by AgentChat to suppress Copilot built-in `@`-tool surfaces
   * (Tools category in typeahead + `+` popover, tool hits in cross-category
   * search). Agent Mode runs its own backend and doesn't use the Copilot
   * tool runner.
   */
  isAgentMode?: boolean;

  /**
   * Installed coding agents the user can `@`-mention this turn (Agent Mode
   * only). Non-empty enables the "Agents" typeahead group and agent pills.
   */
  agentBrands?: ReadonlyArray<AgentMentionBrand>;

  /**
   * Cloud (non-self-hostable) agent backend ids — the full registry set (not
   * just installed agents), so a stale/pasted agent pill still resolves. Drives
   * the Self-Host cloud-egress warning on agent pills.
   */
  cloudAgentIds?: ReadonlySet<string>;

  /**
   * Fires with the backend ids of the agent pills currently in the editor,
   * whenever that set changes. The Agent Mode wrapper resolves these into the
   * structured `mentionedAgents` selection at send time.
   */
  onMentionedAgentsChange?: (backendIds: string[]) => void;
}

/**
 * Imperative handle exposed via `ref`. Lets a wrapper component (e.g.
 * `ChatModeInput`) clear tool pills from the editor without needing
 * direct access to the Lexical instance.
 */
export interface ChatInputHandle {
  removeToolPills(toolNames: string[]): void;
}

const ChatInput = React.forwardRef<ChatInputHandle, ChatInputProps>(function ChatInput(
  {
    topRightAccessory,
    inputMessage,
    setInputMessage,
    handleSendMessage,
    isGenerating,
    onStopGenerating,
    app,
    contextNotes,
    setContextNotes,
    includeActiveNote,
    setIncludeActiveNote,
    includeActiveWebTab,
    setIncludeActiveWebTab,
    activeWebTab,
    selectedImages,
    onAddImage,
    setSelectedImages,
    disableModelSwitch,
    modelPickerOverride,
    modePickerOverride,
    selectedTextContexts,
    onRemoveSelectedText,
    showProgressCard,
    showIndexingCard,
    toolControls,
    onToolPillsChange,
    onTagSelected,
    onEscape,
    onShiftTab,
    editMode = false,
    onEditSave,
    onEditCancel,
    initialContext,
    isAgentMode = false,
    agentBrands = EMPTY_AGENT_MENTION_BRANDS,
    cloudAgentIds = EMPTY_CLOUD_AGENT_IDS,
    onMentionedAgentsChange,
  },
  ref
) {
  const [contextUrls, setContextUrls] = useState<string[]>(initialContext?.urls || []);
  const [contextFolders, setContextFolders] = useState<string[]>(initialContext?.folders || []);
  const [contextWebTabs, setContextWebTabs] = useState<WebTabContext[]>([]);
  const containerRef = useRef<HTMLDivElement>(null);
  const lexicalEditorRef = useRef<LexicalEditorType | null>(null);
  const [currentModelKey, setCurrentModelKey] = useModelKey();
  const [currentChain] = useChainType();
  const [isProjectLoading] = useProjectLoading();
  const [currentActiveNote, setCurrentActiveNote] = useState<TFile | null>(() => {
    const activeFile = app.workspace.getActiveFile();
    return isAllowedFileForNoteContext(activeFile) ? activeFile : null;
  });
  const [notesFromPills, setNotesFromPills] = useState<{ path: string; basename: string }[]>([]);
  const [urlsFromPills, setUrlsFromPills] = useState<string[]>([]);
  const [foldersFromPills, setFoldersFromPills] = useState<string[]>([]);
  const [webTabsFromPills, setWebTabsFromPills] = useState<WebTabContext[]>([]);
  const isCopilotPlus = isPlusChain(currentChain);
  const showAtMentionTools = shouldShowAtMentionTools({ isCopilotPlus, isAgentMode });

  // Merge badge-only contextWebTabs with pills-derived webTabsFromPills for display
  // Uses shared normalization policy from urlNormalization.ts
  const mergedContextWebTabs = useMemo(() => {
    return mergeWebTabContexts([...contextWebTabs, ...webTabsFromPills]);
  }, [contextWebTabs, webTabsFromPills]);

  /**
   * Extract WebTabPillNode data directly from the Lexical editor at send time.
   * This avoids React state synchronization races (webTabsFromPills) when the user sends quickly.
   */
  const getWebTabsFromEditorSnapshot = (): WebTabContext[] => {
    const editor = lexicalEditorRef.current;
    if (!editor) {
      return webTabsFromPills;
    }

    return editor.read((): WebTabContext[] => {
      const pills = $findWebTabPills();
      return pills.map((pill) => ({
        url: pill.getURL(),
        title: pill.getTitle(),
        faviconUrl: pill.getFaviconUrl(),
      }));
    });
  };

  const [loadingMessageIndex, setLoadingMessageIndex] = useState(0);
  const loadingMessages = [
    "Loading the project context...",
    "Processing context files...",
    "If you have many files in context, this can take a while...",
  ];

  const subscribedProject = useSyncExternalStore(subscribeToProjectChange, getCurrentProject);
  const selectedProject = currentChain === ChainType.PROJECT_CHAIN ? subscribedProject : null;

  useEffect(() => {
    if (!isProjectLoading) return;

    const interval = window.setInterval(() => {
      setLoadingMessageIndex((prev) => (prev + 1) % loadingMessages.length);
    }, 3000);

    return () => window.clearInterval(interval);
  }, [isProjectLoading, loadingMessages.length]);

  const getDisplayModelKey = (): string => {
    if (
      selectedProject &&
      currentChain === ChainType.PROJECT_CHAIN &&
      selectedProject.projectModelKey
    ) {
      return selectedProject.projectModelKey;
    }
    return currentModelKey;
  };

  const onSendMessage = () => {
    // Handle edit mode
    if (editMode && onEditSave) {
      onEditSave(inputMessage, {
        notes: contextNotes,
        urls: contextUrls,
        folders: contextFolders,
      });
      return;
    }

    // Combine badge-only web tabs with the send-time Lexical snapshot of WebTab pills.
    // This avoids React state synchronization races when the user sends quickly.
    // Active Web Tab is handled by ChatManager.
    const webTabsFromEditor = getWebTabsFromEditorSnapshot();
    const allWebTabs = mergeWebTabContexts([...contextWebTabs, ...webTabsFromEditor]);

    if (!isCopilotPlus) {
      // Non-Plus chains: only webTabs needs explicit passing
      // - contextNotes: Chat.tsx has state, closure can access
      // - contextFolders: {folderPath} in text gets expanded by processPrompt()
      // - webTabs: passed here, Active Web Tab injected by ChatManager
      handleSendMessage({
        webTabs: allWebTabs,
      });
      return;
    }

    handleSendMessage({
      contextNotes,
      urls: contextUrls,
      contextFolders,
      webTabs: allWebTabs,
    });
  };

  // Handle when pills are removed from the editor
  const handleNotePillsRemoved = (removedNotes: { path: string; basename: string }[]) => {
    const removedPaths = new Set(removedNotes.map((note) => note.path));

    setContextNotes((prev) => {
      return prev.filter((contextNote) => {
        // Remove any note whose pill was removed
        return !removedPaths.has(contextNote.path);
      });
    });
  };

  // Forward the full current set of mentioned backend ids to the Agent Mode
  // wrapper, which resolves the structured selection at send time.
  const handleAgentsChange = useCallback(
    (backendIds: string[]) => {
      onMentionedAgentsChange?.(backendIds);
    },
    [onMentionedAgentsChange]
  );

  // Handle when URLs are removed from pills (when pills are deleted in editor)
  const handleURLPillsRemoved = (removedUrls: string[]) => {
    const removedUrlSet = new Set(removedUrls);

    setContextUrls((prev) => {
      return prev.filter((url) => {
        if (removedUrlSet.has(url)) {
          return false;
        }
        return true;
      });
    });
  };

  // Handle when context notes are removed from the context menu
  // This should remove all corresponding pills from the editor
  const handleContextNoteRemoved = (notePath: string) => {
    if (lexicalEditorRef.current) {
      lexicalEditorRef.current.update(() => {
        $removePillsByPath(notePath);
      });
    }

    // Also immediately update notesFromPills to prevent stale data from re-adding the note
    setNotesFromPills((prev) => prev.filter((note) => note.path !== notePath));
  };

  // Handle when context URLs are removed from the context menu
  // This should remove all corresponding URL pills from the editor
  const handleURLContextRemoved = (url: string) => {
    if (lexicalEditorRef.current) {
      lexicalEditorRef.current.update(() => {
        $removePillsByURL(url);
      });
    }

    // Also immediately update urlsFromPills to prevent stale data from re-adding the URL
    setUrlsFromPills((prev) => prev.filter((pillUrl) => pillUrl !== url));
  };

  // Handle when context folders are removed from the context menu
  // This should remove all corresponding folder pills from the editor
  const handleFolderContextRemoved = (folderPath: string) => {
    if (lexicalEditorRef.current) {
      lexicalEditorRef.current.update(() => {
        $removePillsByFolder(folderPath);
      });
    }

    // Also immediately update foldersFromPills to prevent stale data from re-adding the folder
    setFoldersFromPills((prev) => prev.filter((pillFolder) => pillFolder !== folderPath));
  };

  // Unified handler for adding to context (from popover @ mention)
  const handleAddToContext = (
    category: string,
    data: TFile | string | TFolder | WebTabContext | null
  ) => {
    switch (category) {
      case "activeNote":
        // Set active note context flag (no pill needed - context badge shows it)
        setIncludeActiveNote(true);
        break;
      case "notes":
        if (data instanceof TFile) {
          const activeNote = app.workspace.getActiveFile();
          if (activeNote && data.path === activeNote.path) {
            setIncludeActiveNote(true);
            setContextNotes((prev) => prev.filter((n) => n.path !== data.path));
          } else {
            setContextNotes((prev) => {
              const existingNote = prev.find((n) => n.path === data.path);
              if (existingNote) {
                return prev; // Note already exists, no change needed
              } else {
                return [...prev, data];
              }
            });
          }
        }
        break;
      case "tools":
        // Add tool pill to lexical editor when selected from @ mention typeahead
        if (typeof data === "string" && lexicalEditorRef.current) {
          lexicalEditorRef.current.update(() => {
            // Insert tool pill at current cursor position
            const selection = $getSelection();
            if ($isRangeSelection(selection)) {
              const toolPill = $createToolPillNode(data);
              selection.insertNodes([toolPill]);
            }
          });
          // Note: toolsFromPills will be updated automatically via ToolPillSyncPlugin
        }
        break;
      case "agents":
        // Insert an agent pill at the cursor; agentsFromPills syncs via
        // AgentPillSyncPlugin. `data` is the backend id.
        if (typeof data === "string" && lexicalEditorRef.current) {
          const label = agentBrands.find((b) => b.id === data)?.displayName ?? data;
          lexicalEditorRef.current.update(() => {
            const selection = $getSelection();
            if ($isRangeSelection(selection)) {
              selection.insertNodes([$createAgentPillNode(data, label)]);
            }
          });
        }
        break;
      case "folders":
        // For folders from context menu, update contextFolders directly (no pills in editor)
        if (data && typeof (data as { path?: unknown }).path === "string") {
          const folderPath = (data as { path: string }).path;
          setContextFolders((prev) => {
            const exists = prev.find((f) => f === folderPath);
            if (!exists) {
              return [...prev, folderPath];
            }
            return prev;
          });
        }
        break;
      case "webTabs":
        // Badge-only behavior (like notes): add to contextWebTabs state, no pill insertion
        if (
          data &&
          typeof data === "object" &&
          "url" in data &&
          typeof (data as { url: unknown }).url === "string"
        ) {
          const normalized = normalizeWebTabContext(data);
          if (!normalized) break;

          // If selecting the active web tab, toggle the active badge instead
          const activeUrl = normalizeUrlString(activeWebTab?.url);
          if (activeUrl && normalized.url === activeUrl) {
            setIncludeActiveWebTab(true);
            setContextWebTabs((prev) =>
              prev.filter((t) => normalizeUrlString(t.url) !== activeUrl)
            );
            break;
          }

          setContextWebTabs((prev) => mergeWebTabContexts([...prev, normalized]));
        }
        break;
      case "activeWebTab":
        // Badge-only behavior (like activeNote): toggle include flag, no pill insertion
        setIncludeActiveWebTab(true);
        // Remove from contextWebTabs if it was added as a regular tab
        {
          const activeUrl = normalizeUrlString(activeWebTab?.url);
          if (activeUrl) {
            setContextWebTabs((prev) =>
              prev.filter((t) => normalizeUrlString(t.url) !== activeUrl)
            );
          }
        }
        break;
      case "images": {
        // Use the input's own document so the file picker opens in the window
        // hosting this chat view (popout-safe), not whichever window is focused.
        const doc = containerRef.current?.doc;
        if (!doc) break;
        openImagePicker(doc, {
          onFiles: onAddImage,
          // Restore focus to the composer so cancelling the dialog doesn't
          // leave the pane unresponsive (#119).
          onSettle: () => lexicalEditorRef.current?.focus(),
        });
        break;
      }
    }
  };

  // Unified handler for removing from context (from context menu badges)
  const handleRemoveFromContext = (category: string, data: string) => {
    switch (category) {
      case "activeNote":
        // Remove active note pill from editor and turn off flag
        setIncludeActiveNote(false);
        if (lexicalEditorRef.current) {
          lexicalEditorRef.current.update(() => {
            $removeActiveNotePills();
          });
        }
        break;
      case "notes":
        if (typeof data === "string") {
          // data is the path
          // Check if this is the active note
          if (currentActiveNote?.path === data && includeActiveNote) {
            setIncludeActiveNote(false);
          } else {
            // Remove from contextNotes
            setContextNotes((prev) => prev.filter((note) => note.path !== data));
          }
          // Also remove corresponding pills from editor
          handleContextNoteRemoved(data);
        }
        break;
      case "urls":
        if (typeof data === "string") {
          setContextUrls((prev) => prev.filter((u) => u !== data));
          handleURLContextRemoved(data);
        }
        break;
      case "folders":
        if (typeof data === "string") {
          // data is the path
          setContextFolders((prev) => prev.filter((f) => f !== data));
          handleFolderContextRemoved(data);
        }
        break;
      case "selectedText":
        if (typeof data === "string") {
          // data is the id
          onRemoveSelectedText?.(data);
        }
        break;
      case "activeWebTab":
        // Remove active web tab pill from editor and turn off flag
        setIncludeActiveWebTab(false);
        if (lexicalEditorRef.current) {
          lexicalEditorRef.current.update(() => {
            $removeActiveWebTabPills();
          });
        }
        break;
      case "webTabs":
        // Remove web tab from contextWebTabs state
        if (typeof data === "string") {
          const url = normalizeUrlString(data);
          if (!url) break;

          setContextWebTabs((prev) => prev.filter((t) => normalizeUrlString(t.url) !== url));
          // Also immediately update pills-derived state to avoid UI re-adding during sync lag
          setWebTabsFromPills((prev) => prev.filter((t) => normalizeUrlString(t.url) !== url));
          // Also remove any corresponding pills from editor (if any exist)
          if (lexicalEditorRef.current) {
            lexicalEditorRef.current.update(() => {
              $removeWebTabPillsByUrl(url);
            });
          }
        }
        break;
    }
  };

  // Handle when folders are removed from pills (when pills are deleted in editor)
  const handleFolderPillsRemoved = (removedFolders: string[]) => {
    const removedFolderPaths = new Set(removedFolders);

    setContextFolders((prev) => {
      return prev.filter((folder) => {
        if (removedFolderPaths.has(folder)) {
          return false; // Remove this folder
        }
        return true; // Keep this folder
      });
    });
  };

  // Pill-to-context synchronization (when pills are added)
  useEffect(() => {
    setContextNotes((prev) => {
      const contextPaths = new Set(prev.map((note) => note.path));

      // Find notes that need to be added
      const newNotesFromPills = notesFromPills.filter((pillNote) => {
        // Only add if not already in context
        return !contextPaths.has(pillNote.path);
      });

      // Add completely new notes from pills
      const newFiles: TFile[] = [];
      newNotesFromPills.forEach((pillNote) => {
        const file = app.vault.getAbstractFileByPath(pillNote.path);
        if (file instanceof TFile) {
          newFiles.push(file);
        }
      });

      return [...prev, ...newFiles];
    });
  }, [notesFromPills, app.vault, setContextNotes]);

  // Pill state is owned by the Lexical editor; absorb new entries into our context arrays
  // without removing user-added ones (removal is handled in the dedicated handlers above).
  useEffect(() => {
    if (isPlusChain(currentChain)) {
      // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
      setContextUrls((prev) => {
        const contextUrlSet = new Set(prev);
        const newUrlsFromPills = urlsFromPills.filter((pillUrl) => !contextUrlSet.has(pillUrl));
        if (newUrlsFromPills.length > 0) {
          return Array.from(new Set([...prev, ...newUrlsFromPills]));
        }
        return prev;
      });
    } else {
      // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
      setContextUrls([]);
    }
  }, [urlsFromPills, currentChain]);

  // Pill state is owned by the Lexical editor; absorb new entries into context folders
  // without removing user-added ones (removal is handled in handleFolderPillsRemoved).
  useEffect(() => {
    // eslint-disable-next-line @eslint-react/hooks-extra/no-direct-set-state-in-use-effect
    setContextFolders((prev) => {
      const contextFolderPaths = new Set(prev);
      const newFoldersFromPills = foldersFromPills.filter(
        (pillFolder) => !contextFolderPaths.has(pillFolder)
      );
      return [...prev, ...newFoldersFromPills];
    });
  }, [foldersFromPills]);

  // Update the current active note whenever it changes
  useEffect(() => {
    let timeoutId: number;

    const handleActiveLeafChange = () => {
      // Clear any existing timeout
      window.clearTimeout(timeoutId);

      // Set new timeout
      timeoutId = window.setTimeout(() => {
        const activeNote = app.workspace.getActiveFile();
        setCurrentActiveNote(isAllowedFileForNoteContext(activeNote) ? activeNote : null);
      }, 100); // Wait 100ms after the last event because it fires multiple times
    };

    const eventRef = app.workspace.on("active-leaf-change", handleActiveLeafChange);

    return () => {
      window.clearTimeout(timeoutId); // Clean up any pending timeout
      // cspell:disable-next-line
      app.workspace.offref(eventRef); // Remove event listener
    };
  }, [app.workspace]);

  const onEditorReady = useCallback((editor: LexicalEditorType) => {
    lexicalEditorRef.current = editor;
  }, []);

  // Handle Escape key for edit mode
  useEffect(() => {
    if (!editMode || !onEditCancel) return;

    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") {
        event.preventDefault();
        onEditCancel();
      }
    };

    const doc = containerRef.current?.doc;
    if (!doc) return;
    doc.addEventListener("keydown", handleKeyDown);
    return () => doc.removeEventListener("keydown", handleKeyDown);
  }, [editMode, onEditCancel]);

  useImperativeHandle(
    ref,
    () => ({
      removeToolPills(toolNames: string[]) {
        if (!lexicalEditorRef.current) return;
        lexicalEditorRef.current.update(() => {
          toolNames.forEach((name) => $removePillsByToolName(name));
        });
      },
    }),
    []
  );

  // Active note pill sync callbacks
  const handleActiveNoteAdded = useCallback(() => {
    setIncludeActiveNote(true);
  }, [setIncludeActiveNote]);

  const handleActiveNoteRemoved = useCallback(() => {
    setIncludeActiveNote(false);
  }, [setIncludeActiveNote]);

  // Active web tab pill sync callbacks (mirror activeNote behavior)
  // Active Web Tab URL resolution is now handled by ChatManager at send time
  const handleActiveWebTabAdded = useCallback(() => {
    setIncludeActiveWebTab(true);
  }, [setIncludeActiveWebTab]);

  const handleActiveWebTabRemoved = useCallback(() => {
    setIncludeActiveWebTab(false);
  }, [setIncludeActiveWebTab]);

  return (
    <div
      className={cn(
        "tw-flex tw-w-full tw-flex-col tw-gap-0.5 tw-rounded-md tw-border tw-border-solid tw-border-border tw-px-1 tw-pb-1 tw-pt-2 tw-@container/chat-input",
        modePickerOverride?.value === "plan" &&
          "tw-shadow-[0_0_10px_rgba(var(--color-blue-rgb),0.18)] tw-border-blue/60",
        modePickerOverride?.value === "auto" &&
          "tw-shadow-[0_0_10px_rgba(var(--color-red-rgb),0.18)] tw-border-red/60"
      )}
      ref={containerRef}
    >
      {/* Two columns: the content stack, and (when provided) a structural
          accessory column. A column, not an overlay, so badges, images,
          placeholder, and editor text stay clear of the accessory without
          any of them knowing to avoid it — except for the column's own
          deliberate negative-margin nibble, documented below. */}
      <div className="tw-flex tw-gap-1">
        <div className="tw-flex tw-min-w-0 tw-flex-1 tw-flex-col tw-gap-0.5">
          {/* Hide context controls in edit mode - editing only changes text, not context */}
          {!editMode && (
            <ContextControl
              contextNotes={contextNotes}
              includeActiveNote={includeActiveNote}
              activeNote={currentActiveNote}
              includeActiveWebTab={includeActiveWebTab}
              activeWebTab={activeWebTab}
              contextUrls={contextUrls}
              contextFolders={contextFolders}
              contextWebTabs={mergedContextWebTabs}
              selectedTextContexts={selectedTextContexts}
              showProgressCard={showProgressCard}
              showIndexingCard={showIndexingCard}
              onAddToContext={handleAddToContext}
              onRemoveFromContext={handleRemoveFromContext}
              hideAddContextButton={isAgentMode}
              isAgentMode={isAgentMode}
            />
          )}

          {selectedImages.length > 0 && (
            <div className="selected-images">
              {selectedImages.map((file, index) => (
                <div key={getFileIdentityKey(file)} className="image-preview-container">
                  <img
                    src={URL.createObjectURL(file)}
                    alt={file.name}
                    className="selected-image-preview"
                  />
                  <button
                    type="button"
                    className="remove-image-button"
                    onClick={() => setSelectedImages((prev) => prev.filter((_, i) => i !== index))}
                    title="Remove image"
                  >
                    <X className="tw-size-4" />
                  </button>
                </div>
              ))}
            </div>
          )}

          <div className="tw-relative">
            {isProjectLoading && (
              <div className="tw-absolute tw-inset-0 tw-z-modal tw-flex tw-items-center tw-justify-center tw-bg-primary tw-opacity-80 tw-backdrop-blur-sm">
                <div className="tw-flex tw-items-center tw-gap-2">
                  <Loader2 className="tw-size-4 tw-animate-spin" />
                  <span className="tw-text-sm">{loadingMessages[loadingMessageIndex]}</span>
                </div>
              </div>
            )}
            <LexicalEditor
              value={inputMessage}
              onChange={(value) => setInputMessage(value)}
              onSubmit={onSendMessage}
              onNotesChange={setNotesFromPills}
              onNotesRemoved={handleNotePillsRemoved}
              onActiveNoteAdded={handleActiveNoteAdded}
              onActiveNoteRemoved={handleActiveNoteRemoved}
              onURLsChange={isCopilotPlus ? setUrlsFromPills : undefined}
              onURLsRemoved={isCopilotPlus ? handleURLPillsRemoved : undefined}
              onToolsChange={isCopilotPlus ? onToolPillsChange : undefined}
              onFoldersChange={setFoldersFromPills}
              onFoldersRemoved={handleFolderPillsRemoved}
              onWebTabsChange={setWebTabsFromPills}
              onActiveWebTabAdded={handleActiveWebTabAdded}
              onActiveWebTabRemoved={handleActiveWebTabRemoved}
              agentBrands={agentBrands}
              cloudAgentIds={cloudAgentIds}
              onAgentsChange={handleAgentsChange}
              onEditorReady={onEditorReady}
              onImagePaste={onAddImage}
              onTagSelected={onTagSelected}
              placeholder={
                "Your AI assistant for Obsidian • @ to add context • / for custom prompts"
              }
              disabled={isProjectLoading}
              isCopilotPlus={isCopilotPlus}
              showTools={showAtMentionTools}
              currentActiveFile={currentActiveNote}
              currentChain={currentChain}
              onEscape={onEscape}
              onShiftTab={onShiftTab}
            />
          </div>
        </div>

        {topRightAccessory && (
          // -ml-4 pulls the column 16px left: across the 4px flex gap, the
          // editor's own 8px right-padding strip (px-2, permanently
          // text-free), and 4px into the nominal text box — the trigger's
          // inner padding keeps its glyph clear of that last strip (value
          // eyeballed against the live vault). Accepted trade-offs, both
          // confined to the top-right 24px corner: a click there hits the
          // status trigger instead of the underlying composer content
          // (reasonable for a click that close to the icon), and a scrolling editor's
          // scrollbar top briefly passes under the accessory. If a future
          // review flags this geometry again, point them at this note.
          <div className="-tw-ml-4 tw-w-6 tw-shrink-0 tw-self-start">{topRightAccessory}</div>
        )}
      </div>

      <div className="tw-flex tw-h-7 tw-justify-between tw-gap-1 tw-px-1">
        <div className="tw-flex tw-min-w-0 tw-flex-1 tw-items-center tw-gap-1">
          {!editMode && (
            <AddContextButton
              onSelect={handleAddToContext}
              isCopilotPlus={isCopilotPlus}
              showTools={showAtMentionTools}
              currentActiveFile={currentActiveNote}
              lexicalEditorRef={lexicalEditorRef}
            />
          )}
          {modelPickerOverride?.effortOptionsByModelKey && modelPickerOverride.commitSelection ? (
            // Agent Mode: always use the merged picker, even when the active
            // model has no effort dimension — the user can still switch to
            // one that does, and the picker surfaces the whole catalog.
            <ModelEffortPicker
              override={{
                models: modelPickerOverride.models,
                value: modelPickerOverride.value,
                disabled: modelPickerOverride.disabled,
                effort: modelPickerOverride.effort,
                effortOptionsByModelKey: modelPickerOverride.effortOptionsByModelKey,
                commitSelection: modelPickerOverride.commitSelection,
              }}
              className="tw-min-w-0 tw-max-w-full tw-truncate"
            />
          ) : (
            <ModelSelector
              variant="ghost2"
              size="fit"
              disabled={modelPickerOverride?.disabled ?? disableModelSwitch}
              value={modelPickerOverride?.value ?? getDisplayModelKey()}
              models={modelPickerOverride?.models}
              onChange={
                modelPickerOverride?.onChange ??
                ((modelKey) => {
                  if (currentChain !== ChainType.PROJECT_CHAIN) {
                    setCurrentModelKey(modelKey);
                  }
                })
              }
              className="tw-min-w-0 tw-max-w-full tw-truncate"
            />
          )}
        </div>

        <div className="tw-flex tw-items-center tw-gap-1">
          {!isGenerating && toolControls}
          {modePickerOverride && <ModePicker override={modePickerOverride} />}
          {isGenerating ? (
            <Button
              size="icon"
              className={cn(ACCENT_CIRCLE_BUTTON_CLASS)}
              aria-label="Stop generating"
              onClick={() => onStopGenerating()}
            >
              <Square className="tw-size-3 tw-fill-current" />
            </Button>
          ) : (
            <>
              {editMode && onEditCancel && (
                <Button
                  variant="ghost2"
                  size="fit"
                  className="tw-text-muted"
                  onClick={onEditCancel}
                >
                  <span>cancel</span>
                </Button>
              )}
              {editMode ? (
                <Button
                  variant="ghost2"
                  size="fit"
                  className="tw-text-muted"
                  onClick={() => onSendMessage()}
                >
                  <CornerDownLeft className="!tw-size-3" />
                  <span>save</span>
                </Button>
              ) : (
                <Button
                  size="icon"
                  className={cn(ACCENT_CIRCLE_BUTTON_CLASS)}
                  aria-label="Send"
                  onClick={() => onSendMessage()}
                  disabled={!inputMessage.trim()}
                >
                  <ArrowUp className="tw-size-4" />
                </Button>
              )}
            </>
          )}
        </div>
      </div>
    </div>
  );
});

export default ChatInput;
