import { CustomModel, useModelKey } from "@/aiParams";
import { processCommandPrompt } from "@/commands/customCommandUtils";
import { MenuCommandModal, type ContentState } from "@/components/command-ui";
import {
  MODAL_MIN_HEIGHT_COMPACT,
  MODAL_MIN_HEIGHT_EXPANDED,
} from "@/components/command-ui/constants";
import { SelectionHighlight } from "@/editor/selectionHighlight";
import { createHighlightReplaceGuard, type ReplaceGuard } from "@/editor/replaceGuard";
import { logError } from "@/logger";
import { cleanMessageForCopy, findCustomModel, insertIntoEditor } from "@/utils";
import { computeVerticalPlacement } from "@/utils/panelPlacement";
import { computeSelectionAnchors } from "@/utils/selectionAnchors";
import type { EditorView } from "@codemirror/view";
import { PenLine } from "lucide-react";
import { App, Component, MarkdownRenderer, Notice, MarkdownView, Scope } from "obsidian";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { preprocessAIResponse } from "@/utils/markdownPreprocess";
import { Root } from "react-dom/client";
import { createPluginRoot } from "@/utils/react/createPluginRoot";
import { CustomCommand } from "@/commands/type";
import { useSettingsValue, updateSetting } from "@/settings/model";
import {
  useStreamingChatSession,
  type StreamingChatTurnContext,
} from "@/hooks/use-streaming-chat-session";
import { ABORT_REASON } from "@/constants";

// ============================================================================
// Behavior Config - Replaces mode-based branching
// ============================================================================

/**
 * Model selection scope determines how model changes are persisted.
 * - 'quick-command': Changes persist to quickCommandModelKey (shared with Quick Ask)
 * - 'custom-command': Changes only affect current session (respects command-level config)
 */
export type ModelSelectionScope = "quick-command" | "custom-command";

/**
 * Configuration for modal behavior.
 * This replaces the mode-based branching with explicit configuration.
 */
export interface ModalBehaviorConfig {
  /** Whether to auto-execute the command on open (menu mode: true, quick mode: false) */
  autoExecuteOnOpen: boolean;
  /** Whether to hide ContentArea when state is idle */
  hideContentAreaOnIdle: boolean;
  /** Transform function for first submit (e.g., append note context placeholders) */
  firstSubmitTransform?: (input: string, includeNoteContext: boolean) => string;
  /** Label to display in the modal header */
  commandLabel: string;
  /** Icon to display in the modal header (null for no icon, undefined for default) */
  commandIcon?: React.ReactNode | null;
  /** Whether to show the "Include note context" checkbox */
  showIncludeNoteContext?: boolean;
  /**
   * Model selection scope - determines persistence behavior.
   * - 'quick-command': Persists to quickCommandModelKey (default for Quick Command)
   * - 'custom-command': Only affects current session (default for Custom Commands)
   */
  modelSelectionScope?: ModelSelectionScope;
}

/**
 * Resolves modal behavior defaults and caller overrides.
 */
function resolveBehaviorConfig(
  command: CustomCommand,
  overrides?: Partial<ModalBehaviorConfig>
): ModalBehaviorConfig {
  const defaults: ModalBehaviorConfig = {
    autoExecuteOnOpen: true,
    hideContentAreaOnIdle: false,
    commandLabel: command.title,
    commandIcon: <PenLine className="tw-size-4 tw-text-muted" />,
  };

  return { ...defaults, ...overrides };
}

// ============================================================================
// Content Component
// ============================================================================

interface CustomCommandChatModalContentProps {
  originalText: string;
  command: CustomCommand;
  onInsert: (message: string) => void;
  onReplace: (message: string) => void;
  onClose: () => void;
  systemPrompt?: string;
  initialPosition?: { x: number; y: number };
  /** Bottom-anchor Y for "above" placement (panel grows upward) */
  anchorBottom?: number;
  behaviorConfig?: Partial<ModalBehaviorConfig>;
}

/**
 * Content component for CustomCommandChatModal using the new MenuCommandModal.
 */
function CustomCommandChatModalContent({
  originalText,
  command,
  onInsert,
  onReplace,
  onClose,
  systemPrompt,
  initialPosition,
  anchorBottom,
  behaviorConfig,
}: CustomCommandChatModalContentProps) {
  // Resolve behavior configuration
  const behavior = useMemo(
    () => resolveBehaviorConfig(command, behaviorConfig),
    [command, behaviorConfig]
  );

  // Prevent concurrent submissions (double-Enter / re-entrancy).
  const followUpSubmitLockRef = useRef(false);
  // Track mount state to avoid setState on unmounted component
  const isMountedRef = useRef(true);

  // Cleanup on unmount
  useEffect(() => {
    isMountedRef.current = true;
    return () => {
      isMountedRef.current = false;
    };
  }, []);

  // Reason: Create Component synchronously so renderMarkdown is available
  // on the first render (child effects run before parent effects).
  const obsidianComponentRef = useRef<Component | null>(null);
  if (!obsidianComponentRef.current) {
    const comp = new Component();
    comp.load();
    obsidianComponentRef.current = comp;
  }
  useEffect(() => {
    return () => {
      obsidianComponentRef.current?.unload();
      obsidianComponentRef.current = null;
    };
  }, []);

  // Reason: Snapshot file path at mount time for stable Markdown link resolution.
  // Using dynamic getActiveFile() would resolve links against whichever note
  // the user switches to after opening the modal.
  const filePathSnapshotRef = useRef(app.workspace.getActiveFile()?.path ?? "");

  /**
   * Renders markdown content into a DOM element using Obsidian's MarkdownRenderer.
   * Passed to ContentArea to enable preview mode for completed results.
   */
  const renderMarkdown = useCallback(async (content: string, el: HTMLElement) => {
    const comp = obsidianComponentRef.current;
    if (!comp) return;
    const preprocessed = preprocessAIResponse(content);
    await MarkdownRenderer.renderMarkdown(preprocessed, el, filePathSnapshotRef.current, comp);
  }, []);

  // State
  const [finalText, setFinalText] = useState<string>("");
  const [editedText, setEditedText] = useState<string>("");
  const [isLoading, setIsLoading] = useState(behavior.autoExecuteOnOpen);
  const [followUpValue, setFollowUpValue] = useState("");

  // Model selection
  const [globalModelKey] = useModelKey();
  const settings = useSettingsValue();
  const modelSelectionScope = behavior.modelSelectionScope ?? "custom-command";

  // Determine initial model key based on scope:
  // - quick-command: Use quickCommandModelKey (shared with Quick Ask)
  // - custom-command: Use command's modelKey if set, otherwise global model
  const initialModelKey = useMemo(() => {
    if (modelSelectionScope === "quick-command") {
      // Use ?? to match QuickAskPanel behavior (empty string is valid, only null/undefined falls back)
      return settings.quickCommandModelKey ?? globalModelKey;
    }
    // For custom-command scope, respect command-level config
    // Use || here because empty string means "inherit from global"
    return command.modelKey || globalModelKey;
  }, [modelSelectionScope, settings.quickCommandModelKey, command.modelKey, globalModelKey]);

  const [userSelectedModelKey, setUserSelectedModelKey] = useState(initialModelKey);

  // Handle model change with scope-aware persistence
  const handleModelChange = useCallback(
    (newModelKey: string) => {
      setUserSelectedModelKey(newModelKey);
      // Only persist for quick-command scope (shared with Quick Ask)
      if (modelSelectionScope === "quick-command") {
        updateSetting("quickCommandModelKey", newModelKey);
      }
      // For custom-command scope, changes only affect current session
    },
    [modelSelectionScope]
  );

  // Include note context state (for Quick Command mode)
  // Use local state for immediate UI updates, sync to settings on change
  const [includeNoteContext, setIncludeNoteContext] = useState(
    () => settings.quickCommandIncludeNoteContext
  );

  const handleIncludeNoteContextChange = useCallback((checked: boolean) => {
    setIncludeNoteContext(checked);
    updateSetting("quickCommandIncludeNoteContext", checked);
  }, []);

  // Safely resolve the selected model with fallback to first enabled model
  const resolvedModel = useMemo((): CustomModel | null => {
    try {
      const model = findCustomModel(userSelectedModelKey, settings.activeModels);
      // Treat disabled models as invalid selections (ModelSelector won't present them)
      if (!model.enabled) {
        throw new Error(`Selected model is disabled: ${userSelectedModelKey}`);
      }
      return model;
    } catch {
      // Stale model key can happen when a model is removed/renamed/disabled; don't crash the modal.
      // Avoid side effects during render; notify/log in the effect below.
      return settings.activeModels.find((m) => m.enabled) ?? null;
    }
  }, [userSelectedModelKey, settings.activeModels]);

  // Compute the key for the resolved model
  const resolvedModelKey = useMemo(() => {
    if (!resolvedModel) return null;
    return `${resolvedModel.name}|${resolvedModel.provider}`;
  }, [resolvedModel]);

  // Effective model key for the UI — falls back to user selection when resolution fails.
  const effectiveModelKey = resolvedModelKey ?? userSelectedModelKey;

  // Use shared streaming hook
  const {
    isStreaming,
    streamingText,
    runTurn,
    stop: stopStreaming,
    getLatestStreamingText,
  } = useStreamingChatSession({
    model: resolvedModel,
    systemPrompt: systemPrompt || "",
    excludeThinking: true,
    onNoModel: () => {
      new Notice("No active model is configured. Please configure a model in Copilot settings.");
      setIsLoading(false);
    },
    onNonAbortError: (error) => {
      logError("Error generating response:", error);
      new Notice("Error generating response. Please try again.");
      setIsLoading(false);
    },
  });

  // Track the last input prompt for saving context on stop
  const lastInputPromptRef = useRef<string>("");

  // Sync editedText with finalText when finalText changes. Render-phase tracker
  // preserves user edits made after the last finalText change until the next change.
  const [prevFinalText, setPrevFinalText] = useState(finalText);
  if (prevFinalText !== finalText) {
    setPrevFinalText(finalText);
    if (finalText) {
      setEditedText(finalText);
    }
  }

  // Compute content state for MenuCommandModal
  const contentState: ContentState = useMemo(() => {
    if (isLoading && !isStreaming && !streamingText && !finalText) {
      return { type: "loading" };
    }
    if (isStreaming || streamingText) {
      return { type: "result", text: streamingText || finalText, isStreaming };
    }
    if (finalText) {
      return { type: "result", text: finalText, isStreaming: false };
    }
    return { type: "idle" };
  }, [isLoading, isStreaming, streamingText, finalText]);

  // Track if auto-execute has already run (prevent re-execution on model change)
  const didAutoExecuteRef = useRef(false);

  // Generate initial response (only for autoExecuteOnOpen mode)
  useEffect(() => {
    if (!behavior.autoExecuteOnOpen) return;
    // Only execute once - prevent re-execution when model changes
    if (didAutoExecuteRef.current) return;
    didAutoExecuteRef.current = true;

    let cancelled = false;

    async function generateInitialResponse() {
      try {
        const result = await runTurn(async (ctx: StreamingChatTurnContext) => {
          if (ctx.signal.aborted) return "";
          const prompt = await processCommandPrompt(command.content, originalText);
          lastInputPromptRef.current = prompt;
          return prompt;
        });

        if (!cancelled && result) {
          setFinalText(result);
          lastInputPromptRef.current = "";
        }
      } catch (error) {
        logError("Error in initial response:", error);
      } finally {
        if (!cancelled) {
          setIsLoading(false);
        }
      }
    }

    void generateInitialResponse();

    return () => {
      cancelled = true;
      stopStreaming(ABORT_REASON.UNMOUNT);
    };
  }, [behavior.autoExecuteOnOpen, command.content, originalText, runTurn, stopStreaming]);

  const handleFollowUpSubmit = async () => {
    if (!followUpValue.trim()) return;

    // Prevent concurrent submissions
    if (followUpSubmitLockRef.current) return;
    if (isLoading || isStreaming) return;

    followUpSubmitLockRef.current = true;

    // Clear input and previous result immediately for responsive UI
    const inputValue = followUpValue;
    setFollowUpValue("");
    setFinalText("");
    setEditedText("");

    try {
      setIsLoading(true);

      const result = await runTurn(async (ctx: StreamingChatTurnContext) => {
        if (ctx.signal.aborted) return "";

        // Use ctx.isFirstTurn to determine if this is the first turn
        const isFirstTurn = ctx.isFirstTurn;

        // Apply first submit transform if provided (e.g., append note context placeholders)
        let rawInput = inputValue;
        if (isFirstTurn && behavior.firstSubmitTransform) {
          rawInput = behavior.firstSubmitTransform(rawInput, includeNoteContext);
        }

        // Process prompt (expand placeholders)
        const prompt = await processCommandPrompt(rawInput, originalText, !isFirstTurn);
        lastInputPromptRef.current = prompt;
        return prompt;
      });

      // Guard against unmount during async operation
      if (!isMountedRef.current) return;

      if (result) {
        setFinalText(result);
        lastInputPromptRef.current = "";
      }
    } catch (error) {
      // Handle errors in follow-up submit
      if (error instanceof Error && error.name === "AbortError") {
        // Silently ignore abort errors
      } else {
        logError("Error in follow-up submit:", error);
        if (isMountedRef.current) {
          new Notice("Failed to send message. Please try again.");
        }
      }
    } finally {
      if (isMountedRef.current) {
        setIsLoading(false);
      }
      followUpSubmitLockRef.current = false;
    }
  };

  /**
   * Handle user stop action.
   * The shared hook handles memory persistence on USER_STOPPED automatically.
   * We only need to capture the latest text for UI display.
   */
  const handleStop = useCallback(() => {
    // Reason: Use getLatestStreamingText() to bypass RAF throttle,
    // ensuring we capture the most recent streamed content.
    const latestStreamedText = getLatestStreamingText().trim();

    stopStreaming(ABORT_REASON.USER_STOPPED);

    if (latestStreamedText) {
      setFinalText(latestStreamedText);
    }
    lastInputPromptRef.current = "";
  }, [stopStreaming, getLatestStreamingText]);

  const handleCopy = useCallback(async () => {
    const text = editedText || finalText || streamingText;
    if (!text) return;
    const cleaned = cleanMessageForCopy(text);
    try {
      await navigator.clipboard.writeText(cleaned);
      new Notice("Copied to clipboard");
    } catch {
      new Notice("Failed to copy to clipboard");
    }
  }, [editedText, finalText, streamingText]);

  const handleInsert = () => {
    const text = editedText || finalText || streamingText;
    if (text) onInsert(text);
  };

  const handleReplace = () => {
    const text = editedText || finalText || streamingText;
    if (text) onReplace(text);
  };

  return (
    <MenuCommandModal
      open={true}
      onClose={onClose}
      commandIcon={behavior.commandIcon}
      commandLabel={behavior.commandLabel}
      contentState={contentState}
      editableContent={editedText}
      onEditableContentChange={setEditedText}
      followUpValue={followUpValue}
      onFollowUpChange={setFollowUpValue}
      onFollowUpSubmit={handleFollowUpSubmit}
      selectedModel={effectiveModelKey}
      onSelectModel={handleModelChange}
      onStop={handleStop}
      onCopy={handleCopy}
      onInsert={handleInsert}
      onReplace={handleReplace}
      initialPosition={initialPosition}
      anchorBottom={anchorBottom}
      resizable
      hideContentAreaOnIdle={behavior.hideContentAreaOnIdle}
      includeNoteContext={behavior.showIncludeNoteContext ? includeNoteContext : undefined}
      onIncludeNoteContextChange={
        behavior.showIncludeNoteContext ? handleIncludeNoteContextChange : undefined
      }
      renderMarkdown={renderMarkdown}
    />
  );
}

// ============================================================================
// Modal Class
// ============================================================================

/**
 * Standalone floating modal for CustomCommandChat (not using Obsidian Modal).
 * This avoids the overlay issue caused by Obsidian's Modal class.
 * Positions itself near the cursor/selection like Quick Ask.
 */
export class CustomCommandChatModal {
  private root: Root | null = null;
  private container: HTMLElement | null = null;
  private highlightView: EditorView | null = null;
  private replaceGuard: ReplaceGuard | null = null;
  private scope: Scope | null = null;

  constructor(
    private app: App,
    private configs: {
      selectedText: string;
      command: CustomCommand;
      systemPrompt?: string;
      behaviorConfig?: Partial<ModalBehaviorConfig>;
    }
  ) {}

  /**
   * Resolve the correct window for a given view (or the active view).
   * Reason: In Obsidian popout windows, the global `window` refers to the main window,
   * but the editor lives in a different window. Using ownerDocument.defaultView ensures
   * the modal is positioned relative to the correct window.
   */
  private resolveWindow(view?: MarkdownView | null): Window {
    return view?.containerEl?.win ?? window;
  }

  /**
   * Resolve the correct document for a given view (or the active view).
   * Reason: Same multi-window concern as resolveWindow — the modal container must be
   * appended to the document that owns the triggering view.
   */
  private resolveDocument(view?: MarkdownView | null): Document {
    return view?.containerEl?.doc ?? activeDocument;
  }

  /**
   * Calculate initial position based on cursor/selection in the editor.
   * Strategy: vertical-first (determines placement), then horizontal (depends on placement).
   * - Multi-line selections always center horizontally on the editor.
   * - Space checks use scrollRect (editor visible area), not window.
   * - Horizontal clamp to scrollRect first, then viewport as safety net.
   */
  private getInitialPosition(activeView: MarkdownView | null): {
    x: number;
    y: number;
    anchorBottom?: number;
  } {
    const win = this.resolveWindow(activeView);
    const panelWidth = Math.min(500, win.innerWidth * 0.9);
    // Reason: The actual initial panel height depends on whether ContentArea is shown.
    // Quick Command starts idle with no ContentArea (compact).
    // Custom Commands always show ContentArea (expanded).
    // Using the correct height prevents gaps (above) or overlaps (below).
    const hideContentAreaOnIdle = this.configs.behaviorConfig?.hideContentAreaOnIdle ?? false;
    const panelHeight = hideContentAreaOnIdle
      ? MODAL_MIN_HEIGHT_COMPACT
      : MODAL_MIN_HEIGHT_EXPANDED;
    const margin = 12;
    const gap = 6;

    // Fallback: center on screen
    const fallback = {
      x: Math.max(margin, (win.innerWidth - panelWidth) / 2),
      y: Math.max(margin, (win.innerHeight - panelHeight) / 2),
    };

    if (!activeView?.editor?.cm) {
      return fallback;
    }

    const view = activeView.editor.cm;
    const selection = view.state.selection.main;
    const isCursor = selection.empty;

    // Reason: Dual-anchor model — compute separate top/bottom anchor positions.
    // bottomPos is used for "place below selection", topPos for "place above selection".
    // The shared utility handles the line-start trap correction.
    const anchors = computeSelectionAnchors(selection, view.state.doc);

    // Get coordinates for all anchor positions
    const focusCoords = view.coordsAtPos(anchors.focusPos);
    const bottomCoords = view.coordsAtPos(anchors.bottomPos);
    const topCoords = view.coordsAtPos(anchors.topPos);

    if (!focusCoords && !bottomCoords && !topCoords) {
      return fallback;
    }

    // Check visibility of each anchor against the editor's visible scroll area
    const scrollRect = view.scrollDOM.getBoundingClientRect();
    const isVisible = (coords: { top: number; bottom: number; left: number; right: number }) =>
      coords.bottom >= scrollRect.top &&
      coords.top <= scrollRect.bottom &&
      coords.right >= scrollRect.left &&
      coords.left <= scrollRect.right;

    const visibleFocus = focusCoords && isVisible(focusCoords) ? focusCoords : null;
    const visibleBottom = bottomCoords && isVisible(bottomCoords) ? bottomCoords : null;
    const visibleTop = topCoords && isVisible(topCoords) ? topCoords : null;

    if (!visibleFocus && !visibleBottom && !visibleTop) {
      return fallback;
    }

    // --- Visual multi-line detection ---
    // Reason: Using visual line height comparison instead of logical line numbers
    // correctly handles soft-wrapped lines that span multiple visual rows.
    const caretHeight = Math.min(
      (topCoords?.bottom ?? 0) - (topCoords?.top ?? 0),
      (bottomCoords?.bottom ?? 0) - (bottomCoords?.top ?? 0)
    );
    const isVisualMultiLine =
      !isCursor &&
      topCoords &&
      bottomCoords &&
      Math.abs(topCoords.top - bottomCoords.top) > Math.max(caretHeight / 2, 2);

    // --- Vertical positioning (decides placement first) ---
    // Reason: Extracted to a pure helper (computeVerticalPlacement) so the
    // branching logic is testable without DOM/CodeMirror dependencies.
    const { top: rawTop, anchorBottomY } = computeVerticalPlacement({
      scrollRect,
      visibleBottom,
      visibleTop,
      panelHeight,
      margin,
      gap,
      viewportHeight: win.innerHeight,
    });
    const top = rawTop;

    // --- Horizontal positioning (depends on vertical placement) ---
    let left: number;

    if (isCursor) {
      // Cursor: anchor at cursor position
      const anchor = visibleFocus ?? visibleBottom ?? visibleTop!;
      left = anchor.left;
    } else if (!isVisualMultiLine) {
      // Visual single-line: center panel on the selection span
      // Reason: Use normalized anchor positions instead of raw selection.from/to
      // to handle newline-at-end selections (line-start trap).
      const fromCoords = view.coordsAtPos(anchors.topPos);
      const toCoords = view.coordsAtPos(anchors.bottomPos);
      if (fromCoords && toCoords) {
        const centerX = (fromCoords.left + toCoords.right) / 2;
        left = centerX - panelWidth / 2;
      } else {
        // Coords unavailable — fall back to editor center
        left = (scrollRect.left + scrollRect.right) / 2 - panelWidth / 2;
      }
    } else {
      // Reason: Visual multi-line selections center on the editor regardless of
      // below/above/center placement, preventing left-edge snapping on reverse selections.
      left = (scrollRect.left + scrollRect.right) / 2 - panelWidth / 2;
    }

    // Clamp to editor visible area first, then viewport as safety net
    left = Math.max(scrollRect.left, Math.min(left, scrollRect.right - panelWidth));
    left = Math.max(margin, Math.min(left, win.innerWidth - margin - panelWidth));

    return { x: left, y: top, anchorBottom: anchorBottomY };
  }

  open() {
    // Reason: Push a Scope so user-bound global Cmd/Ctrl+Enter hotkeys don't fire
    // while this modal is open. The Scope handlers return true to consume the event;
    // the React onKeyDown inside MenuCommandModal does the actual Replace/Insert.
    this.scope = new Scope();
    this.scope.register(["Mod"], "Enter", () => true);
    this.scope.register(["Mod", "Shift"], "Enter", () => true);
    this.app.keymap.pushScope(this.scope);

    // Reason: Capture activeView once at open time to ensure document/window/editor
    // all reference the same view. Avoids subtle inconsistencies if the user switches
    // tabs between resolveDocument() and the selection snapshot below.
    const activeView = this.app.workspace.getActiveViewOfType(MarkdownView);

    const doc = this.resolveDocument(activeView);
    this.container = doc.createElement("div");
    this.container.className = "copilot-menu-command-modal-container";
    doc.body.appendChild(this.container);

    this.root = createPluginRoot(this.container, this.app);

    // Capture ReplaceGuard (replaces captureReplaceSnapshot)
    const { selectedText, command, systemPrompt, behaviorConfig } = this.configs;
    let selectedTextSnapshot = selectedText;

    if (activeView?.editor?.cm) {
      const view = activeView.editor.cm;
      const selection = view.state.selection.main;
      const filePath = activeView.file?.path ?? null;
      selectedTextSnapshot = view.state.doc.sliceString(selection.from, selection.to);

      // Show persistent selection highlight
      SelectionHighlight.show(view, selection.from, selection.to);
      this.highlightView = view;

      // Create ReplaceGuard
      this.replaceGuard = createHighlightReplaceGuard({
        editorView: view,
        filePathSnapshot: filePath,
        selectedTextSnapshot,
        getCurrentContext: () => {
          const currentView = this.app.workspace.getActiveViewOfType(MarkdownView);
          return {
            editorView: currentView?.editor?.cm ?? null,
            filePath: currentView?.file?.path ?? null,
          };
        },
      });
    }

    const { anchorBottom, ...initialPosition } = this.getInitialPosition(activeView);

    const handleInsert = (message: string) => {
      void insertIntoEditor(message);
      this.close();
    };

    const handleReplace = (message: string) => {
      if (!this.replaceGuard) {
        new Notice("No selection to replace.");
        return;
      }

      const cleanedMessage = cleanMessageForCopy(message);
      const result = this.replaceGuard.replace(cleanedMessage);

      if (!result.ok) {
        new Notice(result.message ?? "Cannot replace.");
        return;
      }

      new Notice("Message replaced in the active note.");
      this.close();
    };

    const handleClose = () => {
      this.close();
    };

    this.root.render(
      <CustomCommandChatModalContent
        originalText={selectedTextSnapshot}
        command={command}
        onInsert={handleInsert}
        onReplace={handleReplace}
        onClose={handleClose}
        systemPrompt={systemPrompt}
        initialPosition={initialPosition}
        anchorBottom={anchorBottom}
        behaviorConfig={behaviorConfig}
      />
    );
  }

  close() {
    if (this.scope) {
      this.app.keymap.popScope(this.scope);
      this.scope = null;
    }
    // Hide selection highlight
    if (this.highlightView) {
      SelectionHighlight.hide(this.highlightView);
      this.highlightView = null;
    }
    // Clean up ReplaceGuard reference
    this.replaceGuard = null;
    this.root?.unmount();
    this.root = null;
    this.container?.remove();
    this.container = null;
  }
}
