import React, {
  createContext,
  useCallback,
  useContext,
  useMemo,
  useReducer,
  useRef,
} from 'react';
import type { ReactNode } from 'react';

import type {
  ICopilotAction,
  ICopilotMessage,
  ICopilotPopupCustomization,
  ICopilotQuest,
  ICopilotQuestOverview,
  ICopilotQuickActionIcon,
  ICopilotStep,
} from '../service/copilot/copilot.interface';
import { dismissCopilotQuest } from '../service/copilot/copilot.service';

import type { CopilotAction, ICopilotData } from './copilot-helpers';
import { copilotReducer, initialCopilotState } from './copilot-helpers';

/**
 * The copilot slice exposed on its own context: every state field plus the
 * actions and an imperative {@link ICopilotApi.getState} that returns the
 * latest copilot state, replicating zustand's ``getState()``.
 */
export interface ICopilotApi extends ICopilotData {
  /** Store the auth credentials so background calls (dismiss) can authenticate. */
  setAuth: (token: string, clientId: string) => void;
  /** Open the panel. */
  open: () => void;
  /** Collapse the panel. */
  close: () => void;
  /** Toggle the panel open state. */
  toggle: () => void;
  /** Replace the whole thread (used to hydrate persisted history). */
  setMessages: (messages: ICopilotMessage[]) => void;
  /** Store the quest overview and derive the launcher badge/notification. */
  setQuestOverview: (overview: ICopilotQuestOverview) => void;
  /** Dismiss the launcher notification toast (this session only). */
  dismissQuestNotification: () => void;
  /** Permanently dismiss the current nudge quest so it stops surfacing. */
  dismissNudge: () => void;
  /** Open the panel and queue a prompt for the copilot to send (page -> chat). */
  requestCopilotTurn: (prompt: string) => void;
  /** Clear the queued prompt once the panel has sent it. */
  clearPendingPrompt: () => void;
  /** Queue (or clear) a KB draft for the Knowledge Base review modal. */
  setKbDraftFileName: (fileName: string | null) => void;
  /** Queue (or clear) a popup-customization proposal for the Settings page. */
  setPendingPopupCustomization: (
    suggestion: ICopilotPopupCustomization | null
  ) => void;
  /** Queue (or clear) generated quick-action icons for the Settings page. */
  setPendingQuickActionIcons: (icons: ICopilotQuickActionIcon[] | null) => void;
  /** Append a user message to the thread. */
  addUserMessage: (content: string) => void;
  /** Start an empty assistant message that steps and tokens append to. */
  startAssistantMessage: () => void;
  /** Insert or update a tool/thinking step on the last assistant message. */
  upsertStep: (step: ICopilotStep) => void;
  /** Attach quick-action buttons to the last assistant message. */
  setAssistantActions: (actions: ICopilotAction[]) => void;
  /** Remove the quick-action buttons from a message once one is used. */
  consumeMessageActions: (index: number) => void;
  /** Attach a started generation (article or content job) to the last message. */
  setAssistantGeneration: (generation: {
    kind: 'article' | 'content';
    articleId?: string;
    jobId?: string;
  }) => void;
  /** Mark a message's tracked generation as ready/sent (stops its poll). */
  setMessageGenerationReady: (index: number) => void;
  /** Append streamed text to the last assistant message. */
  appendToAssistant: (text: string) => void;
  /** Set the streaming flag. */
  setStreaming: (value: boolean) => void;
  /** Replace the last assistant message with an error notice. */
  failLastAssistant: (message: string) => void;
  /** Clear the thread to start a fresh conversation. */
  reset: () => void;
  /** Imperatively patch fields (replaces zustand's ``setState(partial)``). */
  setState: (patch: Partial<ICopilotData>) => void;
  /** Read the latest copilot state + actions imperatively (replaces ``getState()``). */
  getState: () => ICopilotApi;
}

const noopCopilot: ICopilotApi = {
  ...initialCopilotState,
  setAuth: () => {},
  open: () => {},
  close: () => {},
  toggle: () => {},
  setMessages: () => {},
  setQuestOverview: () => {},
  dismissQuestNotification: () => {},
  dismissNudge: () => {},
  requestCopilotTurn: () => {},
  clearPendingPrompt: () => {},
  setKbDraftFileName: () => {},
  setPendingPopupCustomization: () => {},
  setPendingQuickActionIcons: () => {},
  addUserMessage: () => {},
  startAssistantMessage: () => {},
  upsertStep: () => {},
  setAssistantActions: () => {},
  consumeMessageActions: () => {},
  setAssistantGeneration: () => {},
  setMessageGenerationReady: () => {},
  appendToAssistant: () => {},
  setStreaming: () => {},
  failLastAssistant: () => {},
  reset: () => {},
  setState: () => {},
  getState: () => noopCopilot,
};

const CopilotContext = createContext<ICopilotApi>(noopCopilot);

/** Hook returning the copilot slice of the app context. */
export const useCopilot = (): ICopilotApi => useContext(CopilotContext);

interface CopilotProviderProps {
  children: ReactNode;
}

/**
 * Wraps the app with the Reco copilot state. Holds the reducer-backed slice and
 * exposes both the actions and an imperative ``getState()`` so streamed turns
 * can read the latest thread without going stale between renders.
 *
 * @param props - Component props.
 * @param props.children - The subtree that can read copilot state.
 * @return The provider element.
 */
export const CopilotProvider = ({
  children,
}: CopilotProviderProps): JSX.Element => {
  const [copilotState, dispatch] = useReducer(
    copilotReducer,
    initialCopilotState
  );

  // Holds the full copilot API (state + actions) so getState() returns the
  // latest values together with the actions, exactly like zustand's getState().
  const copilotApiRef = useRef<ICopilotApi>(noopCopilot);

  const getState = useCallback((): ICopilotApi => copilotApiRef.current, []);

  const dispatchCopilot = useCallback(
    (action: CopilotAction) => dispatch(action),
    []
  );

  const setAuth = useCallback(
    (nextToken: string, nextClientId: string) =>
      dispatchCopilot({
        type: 'setAuth',
        token: nextToken,
        clientId: nextClientId,
      }),
    [dispatchCopilot]
  );
  const open = useCallback(
    () => dispatchCopilot({ type: 'open' }),
    [dispatchCopilot]
  );
  const close = useCallback(
    () => dispatchCopilot({ type: 'close' }),
    [dispatchCopilot]
  );
  const toggle = useCallback(
    () => dispatchCopilot({ type: 'toggle' }),
    [dispatchCopilot]
  );
  const setMessages = useCallback(
    (messages: ICopilotMessage[]) =>
      dispatchCopilot({ type: 'setMessages', messages }),
    [dispatchCopilot]
  );
  const setQuestOverview = useCallback(
    (overview: ICopilotQuestOverview) =>
      dispatchCopilot({ type: 'setQuestOverview', overview }),
    [dispatchCopilot]
  );
  const dismissQuestNotification = useCallback(
    () => dispatchCopilot({ type: 'dismissQuestNotification' }),
    [dispatchCopilot]
  );
  const dismissNudge = useCallback(() => {
    const nudge: ICopilotQuest | null = copilotApiRef.current.questNudge;
    if (!nudge) {
      dispatchCopilot({ type: 'dismissQuestNotification' });

      return;
    }
    // Clear the toast immediately, then persist the dismissal and re-apply the
    // refreshed overview so the next-best nudge surfaces.
    dispatchCopilot({ type: 'clearNudgeToast' });
    const { token: copilotToken, clientId: copilotClientId } =
      copilotApiRef.current;
    void dismissCopilotQuest(
      nudge.quest_id,
      copilotToken,
      copilotClientId
    ).then(overview => {
      if (overview) dispatchCopilot({ type: 'setQuestOverview', overview });
    });
  }, [dispatchCopilot]);
  const requestCopilotTurn = useCallback(
    (prompt: string) => dispatchCopilot({ type: 'requestCopilotTurn', prompt }),
    [dispatchCopilot]
  );
  const clearPendingPrompt = useCallback(
    () => dispatchCopilot({ type: 'clearPendingPrompt' }),
    [dispatchCopilot]
  );
  const setKbDraftFileName = useCallback(
    (fileName: string | null) =>
      dispatchCopilot({ type: 'setKbDraftFileName', fileName }),
    [dispatchCopilot]
  );
  const setPendingPopupCustomization = useCallback(
    (suggestion: ICopilotPopupCustomization | null) =>
      dispatchCopilot({ type: 'setPendingPopupCustomization', suggestion }),
    [dispatchCopilot]
  );
  const setPendingQuickActionIcons = useCallback(
    (icons: ICopilotQuickActionIcon[] | null) =>
      dispatchCopilot({ type: 'setPendingQuickActionIcons', icons }),
    [dispatchCopilot]
  );
  const addUserMessage = useCallback(
    (content: string) => dispatchCopilot({ type: 'addUserMessage', content }),
    [dispatchCopilot]
  );
  const startAssistantMessage = useCallback(
    () => dispatchCopilot({ type: 'startAssistantMessage' }),
    [dispatchCopilot]
  );
  const upsertStep = useCallback(
    (step: ICopilotStep) => dispatchCopilot({ type: 'upsertStep', step }),
    [dispatchCopilot]
  );
  const setAssistantActions = useCallback(
    (actions: ICopilotAction[]) =>
      dispatchCopilot({ type: 'setAssistantActions', actions }),
    [dispatchCopilot]
  );
  const consumeMessageActions = useCallback(
    (index: number) =>
      dispatchCopilot({ type: 'consumeMessageActions', index }),
    [dispatchCopilot]
  );
  const setAssistantGeneration = useCallback(
    (generation: {
      kind: 'article' | 'content';
      articleId?: string;
      jobId?: string;
    }) => dispatchCopilot({ type: 'setAssistantGeneration', generation }),
    [dispatchCopilot]
  );
  const setMessageGenerationReady = useCallback(
    (index: number) =>
      dispatchCopilot({ type: 'setMessageGenerationReady', index }),
    [dispatchCopilot]
  );
  const appendToAssistant = useCallback(
    (text: string) => dispatchCopilot({ type: 'appendToAssistant', text }),
    [dispatchCopilot]
  );
  const setStreaming = useCallback(
    (value: boolean) => dispatchCopilot({ type: 'setStreaming', value }),
    [dispatchCopilot]
  );
  const failLastAssistant = useCallback(
    (message: string) =>
      dispatchCopilot({ type: 'failLastAssistant', message }),
    [dispatchCopilot]
  );
  const reset = useCallback(
    () => dispatchCopilot({ type: 'reset' }),
    [dispatchCopilot]
  );
  const setState = useCallback(
    (patch: Partial<ICopilotData>) => dispatchCopilot({ type: 'patch', patch }),
    [dispatchCopilot]
  );

  const copilot = useMemo<ICopilotApi>(
    () => ({
      ...copilotState,
      setAuth,
      open,
      close,
      toggle,
      setMessages,
      setQuestOverview,
      dismissQuestNotification,
      dismissNudge,
      requestCopilotTurn,
      clearPendingPrompt,
      setKbDraftFileName,
      setPendingPopupCustomization,
      setPendingQuickActionIcons,
      addUserMessage,
      startAssistantMessage,
      upsertStep,
      setAssistantActions,
      consumeMessageActions,
      setAssistantGeneration,
      setMessageGenerationReady,
      appendToAssistant,
      setStreaming,
      failLastAssistant,
      reset,
      setState,
      getState,
    }),
    [
      copilotState,
      setAuth,
      open,
      close,
      toggle,
      setMessages,
      setQuestOverview,
      dismissQuestNotification,
      dismissNudge,
      requestCopilotTurn,
      clearPendingPrompt,
      setKbDraftFileName,
      setPendingPopupCustomization,
      setPendingQuickActionIcons,
      addUserMessage,
      startAssistantMessage,
      upsertStep,
      setAssistantActions,
      consumeMessageActions,
      setAssistantGeneration,
      setMessageGenerationReady,
      appendToAssistant,
      setStreaming,
      failLastAssistant,
      reset,
      setState,
      getState,
    ]
  );

  // Keep the imperative-read ref pointed at the latest API object so getState()
  // sees current state and actions during the same render and afterward.
  copilotApiRef.current = copilot;

  return (
    <CopilotContext.Provider value={copilot}>
      {children}
    </CopilotContext.Provider>
  );
};
