/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {
  useCallback,
  useMemo,
  useEffect,
  useState,
  createElement,
} from 'react';
import { type PartListUnion } from '@google/genai';
import process from 'node:process';
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
import type {
  Config,
  ExtensionsStartingEvent,
  ExtensionsStoppingEvent,
  ToolCallConfirmationDetails,
  AgentDefinition,
} from '@google/gemini-cli-core';
import {
  GitService,
  Logger,
  logSlashCommand,
  makeSlashCommandEvent,
  SlashCommandStatus,
  ToolConfirmationOutcome,
  Storage,
  IdeClient,
  coreEvents,
  addMCPStatusChangeListener,
  removeMCPStatusChangeListener,
  MCPDiscoveryState,
  CoreToolCallStatus,
} from '@google/gemini-cli-core';
import { useSessionStats } from '../contexts/SessionContext.js';
import type {
  Message,
  HistoryItemWithoutId,
  SlashCommandProcessorResult,
  HistoryItem,
  ConfirmationRequest,
  IndividualToolCallDisplay,
} from '../types.js';
import { MessageType } from '../types.js';
import type { LoadedSettings } from '../../config/settings.js';
import { type CommandContext, type SlashCommand } from '../commands/types.js';
import { CommandService } from '../../services/CommandService.js';
import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js';
import { FileCommandLoader } from '../../services/FileCommandLoader.js';
import { McpPromptLoader } from '../../services/McpPromptLoader.js';
import { SkillCommandLoader } from '../../services/SkillCommandLoader.js';
import { parseSlashCommand } from '../../utils/commands.js';
import {
  type ExtensionUpdateAction,
  type ExtensionUpdateStatus,
} from '../state/extensions.js';
import {
  LogoutConfirmationDialog,
  LogoutChoice,
} from '../components/LogoutConfirmationDialog.js';
import { runExitCleanup } from '../../utils/cleanup.js';

interface SlashCommandProcessorActions {
  openAuthDialog: () => void;
  openThemeDialog: () => void;
  openEditorDialog: () => void;
  openPrivacyNotice: () => void;
  openSettingsDialog: () => void;
  openSessionBrowser: () => void;
  openModelDialog: () => void;
  openVoiceModelDialog: () => void;
  openAgentConfigDialog: (
    name: string,
    displayName: string,
    definition: AgentDefinition,
  ) => void;
  openPermissionsDialog: (props?: { targetDirectory?: string }) => void;
  quit: (messages: HistoryItem[]) => void;
  setDebugMessage: (message: string) => void;
  toggleCorgiMode: () => void;
  toggleVoiceMode: () => void;
  toggleDebugProfiler: () => void;
  dispatchExtensionStateUpdate: (action: ExtensionUpdateAction) => void;
  addConfirmUpdateExtensionRequest: (request: ConfirmationRequest) => void;
  toggleBackgroundTasks: () => void;
  toggleShortcutsHelp: () => void;
  setText: (text: string) => void;
}

/**
 * Hook to define and process slash commands (e.g., /help, /clear).
 */
export const useSlashCommandProcessor = (
  config: Config | null,
  settings: LoadedSettings,
  addItem: UseHistoryManagerReturn['addItem'],
  clearItems: UseHistoryManagerReturn['clearItems'],
  loadHistory: UseHistoryManagerReturn['loadHistory'],
  refreshStatic: () => void,
  toggleVimEnabled: () => Promise<boolean>,
  setIsProcessing: (isProcessing: boolean) => void,
  actions: SlashCommandProcessorActions,
  extensionsUpdateState: Map<string, ExtensionUpdateStatus>,
  isConfigInitialized: boolean,
  setBannerVisible: (visible: boolean) => void,
  setCustomDialog: (dialog: React.ReactNode | null) => void,
) => {
  const session = useSessionStats();
  const [commands, setCommands] = useState<readonly SlashCommand[] | undefined>(
    undefined,
  );
  const [reloadTrigger, setReloadTrigger] = useState(0);

  const reloadCommands = useCallback(() => {
    setReloadTrigger((v) => v + 1);
  }, []);
  const [confirmationRequest, setConfirmationRequest] = useState<null | {
    prompt: React.ReactNode;
    onConfirm: (confirmed: boolean) => void;
  }>(null);

  const [sessionShellAllowlist, setSessionShellAllowlist] = useState(
    new Set<string>(),
  );
  const gitService = useMemo(() => {
    if (!config?.getProjectRoot()) {
      return;
    }
    return new GitService(config.getProjectRoot(), config.storage);
  }, [config]);

  const logger = useMemo(() => {
    const l = new Logger(
      config?.getSessionId() || '',
      config?.storage ?? new Storage(process.cwd()),
    );
    // The logger's initialize is async, but we can create the instance
    // synchronously. Commands that use it will await its initialization.
    return l;
  }, [config]);

  const [pendingItem, setPendingItem] = useState<HistoryItemWithoutId | null>(
    null,
  );

  const pendingHistoryItems = useMemo(() => {
    const items: HistoryItemWithoutId[] = [];
    if (pendingItem != null) {
      items.push(pendingItem);
    }
    return items;
  }, [pendingItem]);

  const addMessage = useCallback(
    (message: Message) => {
      // Convert Message to HistoryItemWithoutId
      let historyItemContent: HistoryItemWithoutId;
      if (message.type === MessageType.ABOUT) {
        historyItemContent = {
          type: 'about',
          cliVersion: message.cliVersion,
          osVersion: message.osVersion,
          sandboxEnv: message.sandboxEnv,
          modelVersion: message.modelVersion,
          selectedAuthType: message.selectedAuthType,
          gcpProject: message.gcpProject,
          ideClient: message.ideClient,
        };
      } else if (message.type === MessageType.HELP) {
        historyItemContent = {
          type: 'help',
          timestamp: message.timestamp,
        };
      } else if (message.type === MessageType.STATS) {
        historyItemContent = {
          type: 'stats',
          duration: message.duration,
        };
      } else if (message.type === MessageType.MODEL_STATS) {
        historyItemContent = {
          type: 'model_stats',
        };
      } else if (message.type === MessageType.TOOL_STATS) {
        historyItemContent = {
          type: 'tool_stats',
        };
      } else if (message.type === MessageType.QUIT) {
        historyItemContent = {
          type: 'quit',
          duration: message.duration,
        };
      } else if (message.type === MessageType.COMPRESSION) {
        historyItemContent = {
          type: 'compression',
          compression: message.compression,
        };
      } else {
        historyItemContent = {
          type: message.type,
          text: message.content,
        };
      }
      addItem(historyItemContent, message.timestamp.getTime());
    },
    [addItem],
  );
  const commandContext = useMemo(
    (): CommandContext => ({
      services: {
        agentContext: config,
        settings,
        git: gitService,
        logger,
      },
      ui: {
        addItem,
        clear: () => {
          clearItems();
          refreshStatic();
          setBannerVisible(false);
        },
        loadHistory: (history, postLoadInput) => {
          loadHistory(history);
          refreshStatic();
          if (postLoadInput !== undefined) {
            actions.setText(postLoadInput);
          }
        },
        setDebugMessage: actions.setDebugMessage,
        pendingItem,
        setPendingItem,
        toggleCorgiMode: actions.toggleCorgiMode,
        toggleVoiceMode: actions.toggleVoiceMode,
        toggleDebugProfiler: actions.toggleDebugProfiler,
        toggleVimEnabled,
        reloadCommands,
        openAgentConfigDialog: actions.openAgentConfigDialog,
        extensionsUpdateState,
        dispatchExtensionStateUpdate: actions.dispatchExtensionStateUpdate,
        addConfirmUpdateExtensionRequest:
          actions.addConfirmUpdateExtensionRequest,
        setConfirmationRequest,
        removeComponent: () => setCustomDialog(null),
        toggleBackgroundTasks: actions.toggleBackgroundTasks,
        toggleShortcutsHelp: actions.toggleShortcutsHelp,
      },
      session: {
        stats: session.stats,
        sessionShellAllowlist,
      },
    }),
    [
      config,
      settings,
      gitService,
      logger,
      loadHistory,
      addItem,
      clearItems,
      refreshStatic,
      session.stats,
      actions,
      pendingItem,
      setPendingItem,
      setConfirmationRequest,
      toggleVimEnabled,
      sessionShellAllowlist,
      reloadCommands,
      extensionsUpdateState,
      setBannerVisible,
      setCustomDialog,
    ],
  );

  useEffect(() => {
    if (!config) {
      return;
    }

    const listener = () => {
      reloadCommands();
    };
    let isActive = true;
    let activeIdeClient: IdeClient | undefined;

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    (async () => {
      const ideClient = await IdeClient.getInstance();
      if (!isActive) {
        return;
      }
      activeIdeClient = ideClient;
      ideClient.addStatusChangeListener(listener);
    })();

    // Listen for MCP server status changes (e.g. connection, discovery completion)
    // to reload slash commands (since they may include MCP prompts).
    addMCPStatusChangeListener(listener);

    // TODO: Ideally this would happen more directly inside the ExtensionLoader,
    // but the CommandService today is not conducive to that since it isn't a
    // long lived service but instead gets fully re-created based on reload
    // events within this hook.
    const extensionEventListener = (
      _event: ExtensionsStartingEvent | ExtensionsStoppingEvent,
    ) => {
      // We only care once at least one extension has completed
      // starting/stopping
      reloadCommands();
    };
    coreEvents.on('extensionsStarting', extensionEventListener);
    coreEvents.on('extensionsStopping', extensionEventListener);

    return () => {
      isActive = false;
      activeIdeClient?.removeStatusChangeListener(listener);
      removeMCPStatusChangeListener(listener);
      coreEvents.off('extensionsStarting', extensionEventListener);
      coreEvents.off('extensionsStopping', extensionEventListener);
    };
  }, [config, reloadCommands]);

  useEffect(() => {
    const controller = new AbortController();

    // eslint-disable-next-line @typescript-eslint/no-floating-promises
    (async () => {
      const commandService = await CommandService.create(
        [
          new BuiltinCommandLoader(config),
          new SkillCommandLoader(config),
          new McpPromptLoader(config),
          new FileCommandLoader(config),
        ],
        controller.signal,
      );

      if (controller.signal.aborted) {
        return;
      }

      setCommands(commandService.getCommands());
    })();

    return () => {
      controller.abort();
    };
  }, [config, reloadTrigger, isConfigInitialized]);

  const handleSlashCommand = useCallback(
    async (
      rawQuery: PartListUnion,
      oneTimeShellAllowlist?: Set<string>,
      overwriteConfirmed?: boolean,
      addToHistory: boolean = true,
    ): Promise<SlashCommandProcessorResult | false> => {
      if (!commands) {
        return false;
      }
      if (typeof rawQuery !== 'string') {
        return false;
      }

      const trimmed = rawQuery.trim();
      if (!trimmed.startsWith('/') && !trimmed.startsWith('?')) {
        return false;
      }

      const {
        commandToExecute,
        args,
        canonicalPath: resolvedCommandPath,
      } = parseSlashCommand(trimmed, commands);

      // If the input doesn't match any known command, check if MCP servers
      // are still loading (the command might come from an MCP server).
      // Otherwise, treat it as regular text input (e.g. file paths like
      // /home/user/file.txt) and let it be sent to the model.
      if (!commandToExecute) {
        const isMcpLoading =
          config?.getMcpClientManager()?.getDiscoveryState() ===
          MCPDiscoveryState.IN_PROGRESS;
        if (isMcpLoading) {
          setIsProcessing(true);
          if (addToHistory) {
            addItem({ type: MessageType.USER, text: trimmed }, Date.now());
          }
          addMessage({
            type: MessageType.ERROR,
            content: `Unknown command: ${trimmed}. Command might have been from an MCP server but MCP servers are not done loading.`,
            timestamp: new Date(),
          });
          setIsProcessing(false);
          return { type: 'handled' };
        }
        return false;
      }

      setIsProcessing(true);

      if (addToHistory) {
        const userMessageTimestamp = Date.now();
        addItem(
          { type: MessageType.USER, text: trimmed },
          userMessageTimestamp,
        );
      }

      let hasError = false;

      const subcommand =
        resolvedCommandPath.length > 1
          ? resolvedCommandPath.slice(1).join(' ')
          : undefined;

      try {
        if (commandToExecute) {
          if (commandToExecute.action) {
            const fullCommandContext: CommandContext = {
              ...commandContext,
              invocation: {
                raw: trimmed,
                name: commandToExecute.name,
                args,
              },
              overwriteConfirmed,
            };

            // If a one-time list is provided for a "Proceed" action, temporarily
            // augment the session allowlist for this single execution.
            if (oneTimeShellAllowlist && oneTimeShellAllowlist.size > 0) {
              fullCommandContext.session = {
                ...fullCommandContext.session,
                sessionShellAllowlist: new Set([
                  ...fullCommandContext.session.sessionShellAllowlist,
                  ...oneTimeShellAllowlist,
                ]),
              };
            }
            const result = await commandToExecute.action(
              fullCommandContext,
              args,
            );

            if (result) {
              switch (result.type) {
                case 'tool':
                  return {
                    type: 'schedule_tool',
                    toolName: result.toolName,
                    toolArgs: result.toolArgs,
                    postSubmitPrompt: result.postSubmitPrompt,
                  };
                case 'message':
                  addItem(
                    {
                      type:
                        result.messageType === 'error'
                          ? MessageType.ERROR
                          : MessageType.INFO,
                      text: result.content,
                    },
                    Date.now(),
                  );
                  return { type: 'handled' };
                case 'logout':
                  // Show logout confirmation dialog with Login/Exit options
                  setCustomDialog(
                    createElement(LogoutConfirmationDialog, {
                      onSelect: async (choice: LogoutChoice) => {
                        setCustomDialog(null);
                        if (choice === LogoutChoice.LOGIN) {
                          actions.openAuthDialog();
                        } else {
                          await runExitCleanup();
                          process.exit(0);
                        }
                      },
                    }),
                  );
                  return { type: 'handled' };
                case 'dialog':
                  switch (result.dialog) {
                    case 'auth':
                      actions.openAuthDialog();
                      return { type: 'handled' };
                    case 'theme':
                      actions.openThemeDialog();
                      return { type: 'handled' };
                    case 'editor':
                      actions.openEditorDialog();
                      return { type: 'handled' };
                    case 'privacy':
                      actions.openPrivacyNotice();
                      return { type: 'handled' };
                    case 'sessionBrowser':
                      actions.openSessionBrowser();
                      return { type: 'handled' };
                    case 'settings':
                      actions.openSettingsDialog();
                      return { type: 'handled' };
                    case 'model':
                      actions.openModelDialog();
                      return { type: 'handled' };
                    case 'voice-model':
                      actions.openVoiceModelDialog();
                      return { type: 'handled' };
                    case 'agentConfig': {
                      // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
                      const props = result.props as Record<string, unknown>;
                      if (
                        !props ||
                        // eslint-disable-next-line no-restricted-syntax
                        typeof props['name'] !== 'string' ||
                        // eslint-disable-next-line no-restricted-syntax
                        typeof props['displayName'] !== 'string' ||
                        !props['definition']
                      ) {
                        throw new Error(
                          'Received invalid properties for agentConfig dialog action.',
                        );
                      }

                      actions.openAgentConfigDialog(
                        props['name'],
                        props['displayName'],
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
                        props['definition'] as AgentDefinition,
                      );
                      return { type: 'handled' };
                    }
                    case 'permissions':
                      actions.openPermissionsDialog(
                        // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
                        result.props as { targetDirectory?: string },
                      );
                      return { type: 'handled' };
                    case 'help':
                      return { type: 'handled' };
                    default: {
                      const unhandled: never = result.dialog;
                      throw new Error(
                        `Unhandled slash command result: ${unhandled}`,
                      );
                    }
                  }
                case 'load_history': {
                  config?.getGeminiClient()?.setHistory(result.clientHistory);
                  fullCommandContext.ui.clear();
                  result.history.forEach((item, index) => {
                    fullCommandContext.ui.addItem(item, index);
                  });
                  return { type: 'handled' };
                }
                case 'quit':
                  if (result.deleteSession) {
                    try {
                      const chatRecordingService = config
                        ?.getGeminiClient()
                        ?.getChatRecordingService();
                      if (chatRecordingService) {
                        await chatRecordingService.deleteCurrentSessionAsync();
                      }
                    } catch {
                      // Don't let deletion errors prevent exit.
                    }
                  }
                  actions.quit(result.messages);
                  return { type: 'handled' };

                case 'submit_prompt':
                  return {
                    type: 'submit_prompt',
                    content: result.content,
                  };
                case 'confirm_shell_commands': {
                  const callId = `expansion-${Date.now()}`;
                  const { outcome, approvedCommands } = await new Promise<{
                    outcome: ToolConfirmationOutcome;
                    approvedCommands?: string[];
                  }>((resolve) => {
                    const confirmationDetails: ToolCallConfirmationDetails = {
                      type: 'exec',
                      title: `Confirm Shell Expansion`,
                      command: result.commandsToConfirm[0] || '',
                      rootCommand: result.commandsToConfirm[0] || '',
                      rootCommands: result.commandsToConfirm,
                      commands: result.commandsToConfirm,
                      onConfirm: async (resolvedOutcome) => {
                        // Close the pending tool display by resolving
                        resolve({
                          outcome: resolvedOutcome,
                          approvedCommands:
                            resolvedOutcome === ToolConfirmationOutcome.Cancel
                              ? []
                              : result.commandsToConfirm,
                        });
                      },
                    };

                    const toolDisplay: IndividualToolCallDisplay = {
                      callId,
                      name: 'Expansion',
                      description: 'Command expansion needs shell access',
                      status: CoreToolCallStatus.AwaitingApproval,
                      isClientInitiated: true,
                      resultDisplay: undefined,
                      confirmationDetails,
                    };

                    setPendingItem({
                      type: 'tool_group',
                      tools: [toolDisplay],
                    });
                  });

                  setPendingItem(null);

                  if (
                    outcome === ToolConfirmationOutcome.Cancel ||
                    !approvedCommands ||
                    approvedCommands.length === 0
                  ) {
                    addItem(
                      {
                        type: MessageType.INFO,
                        text: 'Slash command shell execution declined.',
                      },
                      Date.now(),
                    );
                    return { type: 'handled' };
                  }

                  if (outcome === ToolConfirmationOutcome.ProceedAlways) {
                    setSessionShellAllowlist(
                      (prev) => new Set([...prev, ...approvedCommands]),
                    );
                  }

                  return await handleSlashCommand(
                    result.originalInvocation.raw,
                    // Pass the approved commands as a one-time grant for this execution.
                    new Set(approvedCommands),
                    undefined,
                    false, // Do not add to history again
                  );
                }
                case 'confirm_action': {
                  const { confirmed } = await new Promise<{
                    confirmed: boolean;
                  }>((resolve) => {
                    setConfirmationRequest({
                      prompt: result.prompt,
                      onConfirm: (resolvedConfirmed) => {
                        setConfirmationRequest(null);
                        resolve({ confirmed: resolvedConfirmed });
                      },
                    });
                  });

                  if (!confirmed) {
                    addItem(
                      {
                        type: MessageType.INFO,
                        text: 'Operation cancelled.',
                      },
                      Date.now(),
                    );
                    return { type: 'handled' };
                  }

                  return await handleSlashCommand(
                    result.originalInvocation.raw,
                    undefined,
                    true,
                  );
                }
                case 'custom_dialog': {
                  setCustomDialog(result.component);
                  return { type: 'handled' };
                }
                default: {
                  const unhandled: never = result;
                  throw new Error(
                    `Unhandled slash command result: ${unhandled}`,
                  );
                }
              }
            }

            return { type: 'handled' };
          } else if (commandToExecute.subCommands) {
            const helpText = `Command '/${commandToExecute.name}' requires a subcommand. Available:\n${commandToExecute.subCommands
              .map((sc) => `  - ${sc.name}: ${sc.description || ''}`)
              .join('\n')}`;
            addMessage({
              type: MessageType.INFO,
              content: helpText,
              timestamp: new Date(),
            });
            return { type: 'handled' };
          }
        }

        return { type: 'handled' };
      } catch (e: unknown) {
        hasError = true;
        if (config) {
          const event = makeSlashCommandEvent({
            command: resolvedCommandPath[0],
            subcommand,
            status: SlashCommandStatus.ERROR,
            extension_id: commandToExecute?.extensionId,
          });
          logSlashCommand(config, event);
        }
        addItem(
          {
            type: MessageType.ERROR,
            text: e instanceof Error ? e.message : String(e),
          },
          Date.now(),
        );
        return { type: 'handled' };
      } finally {
        if (config && resolvedCommandPath[0] && !hasError) {
          const event = makeSlashCommandEvent({
            command: resolvedCommandPath[0],
            subcommand,
            status: SlashCommandStatus.SUCCESS,
            extension_id: commandToExecute?.extensionId,
          });
          logSlashCommand(config, event);
        }
        setIsProcessing(false);
      }
    },
    [
      config,
      addItem,
      actions,
      commands,
      commandContext,
      addMessage,
      setSessionShellAllowlist,
      setIsProcessing,
      setConfirmationRequest,
      setCustomDialog,
    ],
  );

  return {
    handleSlashCommand,
    slashCommands: commands,
    pendingHistoryItems,
    commandContext,
    confirmationRequest,
  };
};
