// Typings
import {
  DialogState,
  Memori,
  Integration,
  Venue,
  Message,
  Medium,
  OpenSession,
  MemoriConfig,
  TranslatedHint,
  Tenant,
  MemoriSession,
  User,
  ExpertReference,
  ResponseSpec,
  ChatLog,
} from '@memori.ai/memori-api-client/src/types';
import { ArtifactData } from '../MemoriArtifactSystem/types/artifact.types';
import { ArtifactAPIBridge } from '../MemoriArtifactSystem/utils/ArtifactAPI';
import type { LayoutName, PiiDetectionConfig } from '../../types/layout';
import { checkPii } from '../../helpers/piiDetection'; // PII check when integrationConfig.layout has piiDetection.enabled

// Libraries
import React, {
  useState,
  useEffect,
  useCallback,
  CSSProperties,
  useRef,
  useMemo,
} from 'react';
import { useTranslation } from 'react-i18next';
import memoriApiClient from '@memori.ai/memori-api-client';
import { IAudioContext } from 'standardized-audio-context';
import cx from 'classnames';
import { DateTime } from 'luxon';
import toast from 'react-hot-toast';

// Components
import PositionDrawer from '../PositionDrawer/PositionDrawer';
import MemoriAuth from '../Auth/Auth';
import Chat, { Props as ChatProps } from '../Chat/Chat';
import StartPanel, { Props as StartPanelProps } from '../StartPanel/StartPanel';
import Avatar, { Props as AvatarProps } from '../Avatar/Avatar';
import Header, { Props as HeaderProps } from '../Header/Header';
import PoweredBy from '../PoweredBy/PoweredBy';
import AgeVerificationModal from '../AgeVerificationModal/AgeVerificationModal';
import SettingsDrawer from '../SettingsDrawer/SettingsDrawer';
import KnownFacts from '../KnownFacts/KnownFacts';
import ExpertsDrawer from '../ExpertsDrawer/ExpertsDrawer';
import LoginDrawer from '../LoginDrawer/LoginDrawer';
import Button from '../ui/Button';
import CloseIcon from '../icons/Close';

// Layout
import FullPageLayout from '../layouts/FullPage';
import TotemLayout from '../layouts/Totem';
import ChatLayout from '../layouts/Chat';
import WebsiteAssistantLayout from '../layouts/WebsiteAssistant';
import HiddenChatLayout from '../layouts/HiddenChat';
import ZoomedFullBodyLayout from '../layouts/ZoomedFullBody';

// Helpers / Utils
import { getTranslation } from '../../helpers/translations';
import {
  setLocalConfig,
  getLocalConfig,
  removeLocalConfig,
} from '../../helpers/configuration';
import {
  hasTouchscreen,
  stripDuplicates,
  installMathJax,
} from '../../helpers/utils';
import { getTTSVoice } from '../../helpers/tts/ttsVoiceUtility';
import {
  allowedMediaTypes,
  anonTag,
  uiLanguages,
} from '../../helpers/constants';
import { getErrori18nKey } from '../../helpers/error';
import { getCredits } from '../../helpers/credits';
import { sanitizeText } from '../../helpers/sanitizer';
import { TTSConfig, useTTS } from '../../helpers/tts/useTTS';
import ChatHistoryDrawer from '../ChatHistoryDrawer/ChatHistory';
import { STTConfig, useSTT } from '../../helpers/stt/useSTT';
import { useNats } from '../../helpers/nats/useNats';
import {
  NatsProgressEvent,
  NatsDialogResponseEvent,
  NatsErrorEvent,
} from '../../helpers/nats/useNatsSession';
import {
  isSessionExpiredNatsError,
  isSessionExpiredNatsResponse,
} from '../../helpers/nats/isSessionExpiredError';

// Widget utilities and helpers
const getMemoriState = (integrationId?: string): object | null => {
  let widget = integrationId
    ? document.querySelector(
        `.memori-widget[data-memori-integration="${integrationId}"]`
      ) ||
      document
        .querySelector('memori-client')
        ?.shadowRoot?.querySelector(`.memori-widget[data-memori-integration]`)
    : document.querySelector('.memori-widget') ||
      document
        .querySelector('memori-client')
        ?.shadowRoot?.querySelector('.memori-widget');

  if (!widget) return null;

  let engineState = (widget as HTMLElement).dataset?.memoriEngineState;
  if (!engineState) return null;

  let dialogState = JSON.parse(engineState);

  let loginToken = getLocalConfig<string | undefined>('loginToken', undefined);

  return {
    ...dialogState,
    loginToken,
  };
};

/** Place spec with all nulls for postEnterTextAsync when position is not set or user chose "I don't want to provide my position". */
const NULL_PLACE_SPEC = {
  placeName: null,
  latitude: null,
  longitude: null,
  uncertaintyKm: null,
} as const;

const logWidgetError = (context: string, detail?: unknown) => {
  console.error(`[MemoriWidget] ${context}`, detail ?? '');
};

/** Reads correlation id from HTTP async response (supports camelCase / snake_case). */
function readCorrelationID(response: {
  correlationID?: string;
}): string | undefined {
  const value = response.correlationID;
  return typeof value === 'string' && value.length > 0 ? value : undefined;
}

type MemoriTextEnteredEvent = CustomEvent<{
  text: string;
  waitForPrevious?: boolean;
  hidden?: boolean;
  typingText?: string;
  useLoaderTextAsMsg?: boolean;
  hasBatchQueued?: boolean;
}>;

/**
 * Dispatches a MemoriTextEntered event to simulate a user typing a message
 * @param message The text message to send
 * @param waitForPrevious Whether to wait for previous message to finish before sending (default true)
 * @param hidden Whether to hide the message from chat history (default false)
 * @param typingText Optional custom typing indicator text
 * @param useLoaderTextAsMsg Whether to use the loader text as the message (default false)
 * @param hasBatchQueued Whether there are more messages queued to be sent (default false)
 */
const typeMessage = (
  message: string,
  waitForPrevious = true,
  hidden = false,
  typingText?: string,
  useLoaderTextAsMsg = false,
  hasBatchQueued = false
) => {
  const e: MemoriTextEnteredEvent = new CustomEvent('MemoriTextEntered', {
    detail: {
      text: message,
      waitForPrevious,
      hidden,
      typingText,
      useLoaderTextAsMsg,
      hasBatchQueued,
    },
  });
  document.dispatchEvent(e);

  // Special handling for Safari on iOS devices
  const isSafariIOS =
    window.navigator.userAgent.includes('Safari') &&
    !window.navigator.userAgent.includes('Chrome') &&
    /iPad|iPhone|iPod/.test(navigator.userAgent);

  if (isSafariIOS) {
    // Dispatch end speak event after short delay for iOS Safari
    setTimeout(() => {
      document.dispatchEvent(new CustomEvent('MemoriEndSpeak'));
    }, 300);
  }
};

/**
 * Helper function to send a hidden message
 * Wraps typeMessage with hidden=true and passes through other params
 */
const typeMessageHidden = (
  message: string,
  waitForPrevious = true,
  typingText?: string,
  useLoaderTextAsMsg = false,
  hasBatchQueued = false
) =>
  typeMessage(
    message,
    waitForPrevious,
    true,
    typingText,
    useLoaderTextAsMsg,
    hasBatchQueued
  );

const typeBatchMessages = (
  messages: {
    message: string;
    waitForPrevious?: boolean;
    hidden?: boolean;
    typingText?: string;
    useLoaderTextAsMsg?: boolean;
  }[]
) => {
  function disableInputs() {
    document
      .querySelector('fieldset#chat-fieldset')
      ?.setAttribute('disabled', '');

    const styles = `opacity: 0.5; touch-action: none; pointer-events: none;`;
    document
      .querySelector('textarea.memori-chat-textarea--input')
      ?.setAttribute('style', styles);
    document
      .querySelector('button.memori-chat-inputs--send')
      ?.setAttribute('style', styles);
    document
      .querySelector('button.memori-chat-inputs--mic')
      ?.setAttribute('style', styles);
  }

  function reEnableInputs() {
    document
      .querySelector('fieldset#chat-fieldset')
      ?.removeAttribute('disabled');

    document
      .querySelector('textarea.memori-chat-textarea--input')
      ?.removeAttribute('style');
    document
      .querySelector('button.memori-chat-inputs--send')
      ?.removeAttribute('style');
    document
      .querySelector('button.memori-chat-inputs--mic')
      ?.removeAttribute('style');
  }

  function areInputsDisabled() {
    return !!document
      .querySelector('fieldset#chat-fieldset')
      ?.hasAttribute('disabled');
  }

  const isSafariIOS =
    window.navigator.userAgent.includes('Safari') &&
    !window.navigator.userAgent.includes('Chrome') &&
    /iPad|iPhone|iPod/.test(navigator.userAgent);

  const stepsGenerator = (function* () {
    yield* messages;
  })();

  disableInputs();

  const submitNewMessage = () => {
    const next = stepsGenerator.next();
    const step = next.value;

    if (step) {
      if (!areInputsDisabled()) {
        disableInputs();
      }

      let waitForPrevious = step.waitForPrevious;
      if (isSafariIOS) waitForPrevious = false;

      typeMessage(
        step.message,
        waitForPrevious,
        step.hidden,
        step.typingText,
        step.useLoaderTextAsMsg,
        !next.done
      );

      if (isSafariIOS) {
        setTimeout(() => {
          document.dispatchEvent(new CustomEvent('MemoriEndSpeak'));
          reEnableInputs();
        }, 3000);
      }
    } else if (areInputsDisabled()) {
      reEnableInputs();
    }

    if (next.done) {
      document.removeEventListener('MemoriEndSpeak', submitNewMessage);
      if (areInputsDisabled()) reEnableInputs();
      return;
    }
  };

  document.addEventListener('MemoriEndSpeak', submitNewMessage);

  submitNewMessage();
};

type MemoriNewDialogStateEvent = CustomEvent<DialogState>;

type ArtifactCreatedEvent = CustomEvent<{
  artifact: ArtifactData;
  message: Message;
}>;

interface CustomEventMap {
  MemoriTextEntered: MemoriTextEnteredEvent;
  MemoriEndSpeak: CustomEvent;
  MemoriResetUIEffects: CustomEvent;
  MemoriNewDialogState: MemoriNewDialogStateEvent;
  artifactCreated: ArtifactCreatedEvent;
}
declare global {
  interface Document {
    addEventListener<K extends keyof CustomEventMap>(
      type: K,
      listener: (this: Document, ev: CustomEventMap[K]) => void
    ): void;
    removeEventListener<K extends keyof CustomEventMap>(
      type: K,
      listener: (this: Document, ev: CustomEventMap[K]) => void
    ): void;
    dispatchEvent<K extends keyof CustomEventMap>(ev: CustomEventMap[K]): void;
  }

  interface Window {
    getMemoriState: typeof getMemoriState;
    typeMessage: typeof typeMessage;
    typeMessageHidden: typeof typeMessageHidden;
    typeBatchMessages: typeof typeBatchMessages;
    MemoriArtifactAPI?: {
      openArtifact: (artifact: ArtifactData) => void;
      createAndOpenArtifact: (
        content: string,
        mimeType?: string,
        title?: string
      ) => void;
      createFromOutputElement: (outputElement: HTMLOutputElement) => string;
      closeArtifact: () => void;
      toggleFullscreen: () => void;
      getState: () => {
        currentArtifact: ArtifactData | null;
        isDrawerOpen: boolean;
        isFullscreen: boolean;
      };
    };
  }
}
window.getMemoriState = getMemoriState;
window.typeMessage = typeMessage;
window.typeMessageHidden = typeMessageHidden;
window.typeBatchMessages = typeBatchMessages;

let audioContext: IAudioContext;

let memoriPassword: string | undefined;
let userToken: string | undefined;

export interface LayoutProps {
  Header?: typeof Header;
  headerProps?: HeaderProps;
  Avatar: typeof Avatar;
  avatarProps?: AvatarProps;
  Chat?: typeof Chat;
  chatProps?: ChatProps;
  StartPanel: typeof StartPanel;
  startPanelProps?: StartPanelProps;
  integrationStyle?: JSX.Element | null;
  integrationBackground?: JSX.Element | null;
  poweredBy?: JSX.Element | null;
  sessionId?: string;
  hasUserActivatedSpeak?: boolean;
  showUpload?: boolean;
  loading?: boolean;
  autoStart?: boolean;
  onSidebarToggle?: (isOpen: boolean) => void;
  /** When true or "true" (e.g. from integrationConfig or web component attribute), hide the 3D avatar. */
  avatar3dHidden?: boolean | string;
}

export interface Props {
  memori: Memori;
  ownerUserName?: string | null;
  ownerUserID?: string | null;
  tenantID: string;
  memoriConfigs?: MemoriConfig[];
  memoriLang?: string;
  /** UI language: labels, buttons, page translation (i18n) */
  uiLang?: string;
  /** Spoken/chat language: select box in StartPanel and conversation language */
  spokenLang?: string;
  multilingual?: boolean;
  integration?: Integration;
  layout?: LayoutName;
  customLayout?: React.FC<LayoutProps>;
  showShare?: boolean;
  showCopyButton?: boolean;
  showTranslationOriginal?: boolean;
  showInputs?: boolean;
  showDates?: boolean;
  showContextPerLine?: boolean;
  showMessageConsumption?: boolean;
  showSettings?: boolean;
  showClear?: boolean;
  showOnlyLastMessages?: boolean;
  showTypingText?: boolean;
  showLogin?: boolean;
  showUpload?: boolean;
  showChatHistory?: boolean;
  showReasoning?: boolean;
  /** When true and layout is WEBSITE_ASSISTANT, hide the 3D avatar in the expanded panel. */
  avatar3dHidden?: boolean;
  preview?: boolean;
  embed?: boolean;
  height?: number | string;
  secret?: string;
  baseUrl?: string;
  apiURL?: string;
  engineURL?: string;
  initialContextVars?: { [key: string]: string };
  initialQuestion?: string;
  ogImage?: string;
  sessionID?: string;
  tenant?: Tenant;
  personification?: {
    name?: string;
    tag: string;
    pin: string;
  };
  ttsProvider?: 'azure' | 'openai';
  enableAudio?: boolean;
  defaultSpeakerActive?: boolean;
  disableTextEnteredEvents?: boolean;
  onStateChange?: (state?: DialogState) => void;
  additionalInfo?: OpenSession['additionalInfo'] & { [key: string]: string };
  customMediaRenderer?: ChatProps['customMediaRenderer'];
  additionalSettings?: JSX.Element | null;
  userAvatar?: string | JSX.Element;
  useMathFormatting?: boolean;
  autoStart?: boolean;
  applyVarsToRoot?: boolean;
  showFunctionCache?: boolean;
  authToken?: string;
  __WEBCOMPONENT__?: boolean;
  /** Override total document payload and per-document content limit (character count). Default from constants. */
  maxTotalMessagePayload?: number;
  /** Max characters in chat textarea; shows counter and enforces paste + existing text does not exceed this limit. */
  maxTextareaCharacters?: number;
}

const MemoriWidget = ({
  memori,
  memoriConfigs,
  ownerUserID,
  ownerUserName,
  tenantID,
  memoriLang,
  uiLang,
  spokenLang,
  multilingual,
  integration,
  layout,
  customLayout,
  showShare,
  preview = false,
  embed = false,
  showCopyButton = true,
  showTranslationOriginal = false,
  showInputs = true,
  showDates = false,
  showContextPerLine = false,
  showMessageConsumption = false,
  showSettings,
  showTypingText = false,
  showClear = false,
  showLogin = false,
  showUpload,
  showOnlyLastMessages,
  showChatHistory,
  showReasoning,
  avatar3dHidden,
  height = '100vh',
  secret,
  baseUrl = 'https://aisuru-staging.aclambda.online',
  apiURL = 'https://backend-staging.memori.ai',
  engineURL = 'https://engine-staging.memori.ai',
  initialContextVars,
  initialQuestion,
  ttsProvider,
  ogImage,
  sessionID: initialSessionID,
  tenant,
  personification,
  authToken,
  enableAudio,
  defaultSpeakerActive = true,
  disableTextEnteredEvents = false,
  onStateChange,
  additionalInfo,
  additionalSettings,
  customMediaRenderer,
  userAvatar,
  __WEBCOMPONENT__ = false,
  useMathFormatting = false,
  autoStart = false,
  applyVarsToRoot = false,
  showFunctionCache = false,
  maxTotalMessagePayload,
  maxTextareaCharacters,
}: Props) => {
  const { t, i18n } = useTranslation();

  const [isClient, setIsClient] = useState(false);
  useEffect(() => {
    setIsClient(true);
  }, []);

  // API calls methods
  const client = memoriApiClient(apiURL, engineURL);
  const {
    initSession,
    deleteSession,
    postEnterTextAsync,
    postTextEnteredEvent,
    postPlaceChangedEvent,
    postDateChangedEvent,
    postTagChangedEvent,
    getSession,
    getExpertReferences,
    getSessionChatLogs,
  } = client;

  const [instruct, setInstruct] = useState(false);
  const [enableFocusChatInput, setEnableFocusChatInput] = useState(true);

  const [loginToken, setLoginToken] = useState<string | undefined>(
    additionalInfo?.loginToken ?? authToken
  );
  const [user, setUser] = useState<User | undefined>({
    avatarURL: typeof userAvatar === 'string' ? userAvatar : undefined,
  } as User);
  useEffect(() => {
    if (
      loginToken &&
      !user?.userID &&
      (showLogin || memori.requireLoginToken)
    ) {
      client.backend
        .pwlGetCurrentUser(loginToken)
        .then(({ user, resultCode }) => {
          if (user && resultCode === 0) {
            setUser(user);
            setLocalConfig('loginToken', loginToken);

            if (!birthDate && user.birthDate) {
              setBirthDate(user.birthDate);
              setLocalConfig('birthDate', user.birthDate);
            }
          } else {
            removeLocalConfig('loginToken');
          }
        });
    }
  }, [loginToken, user?.userID]);
  const [showLoginDrawer, setShowLoginDrawer] = useState(false);

  const [clickedStart, setClickedStart] = useState(false);
  const sessionStartingRef = useRef(false);

  const language =
    memori.culture?.split('-')?.[0]?.toUpperCase()! ||
    memoriConfigs
      ?.find(c => c.memoriConfigID === memori.memoriConfigurationID)
      ?.culture?.split('-')?.[0]
      ?.toUpperCase()!;
  const integrationConfig = integration?.customData
    ? JSON.parse(integration.customData)
    : null;

  const isMultilanguageEnabled =
    multilingual !== undefined
      ? multilingual
      : !!integrationConfig?.multilanguage;
  const forcedTimeout = integrationConfig?.forcedTimeout as number | undefined;
  const [userLang, setUserLang] = useState(
    spokenLang ??
      memoriLang ??
      integrationConfig?.lang ??
      language ??
      integrationConfig?.uiLang ??
      i18n.language ??
      'IT'
  );

  // Sync userLang when parent passes spokenLang (select box in StartPanel)
  useEffect(() => {
    if (spokenLang != null) {
      setUserLang(spokenLang);
    }
  }, [spokenLang]);

  const applyMathFormatting =
    useMathFormatting !== undefined
      ? useMathFormatting
      : !!integrationConfig?.useMathFormatting;
  useEffect(() => {
    if (applyMathFormatting) installMathJax();
  }, [applyMathFormatting]);

  /**
   * Sets the UI language in the i18n instance (page translation). uiLang takes precedence.
   */
  useEffect(() => {
    const langToApply =
      uiLang && uiLanguages.includes(uiLang.toLowerCase())
        ? uiLang.toLowerCase()
        : userLang && uiLanguages.includes(userLang.toLowerCase())
        ? userLang.toLowerCase()
        : null;
    if (langToApply && typeof i18n?.changeLanguage === 'function') {
      // @ts-ignore
      i18n.changeLanguage(langToApply);
    }
  }, [uiLang, userLang]);

  const [loading, setLoading] = useState(false);
  const [memoriTyping, setMemoriTyping] = useState<boolean>(false);
  const [typingText, setTypingText] = useState<string>();

  type EnterTextRetryParams = {
    text?: string;
    media?: Medium[];
    translate?: boolean;
    translatedText?: string;
    hidden?: boolean;
    typingText?: string;
    useLoaderTextAsMsg?: boolean;
    hasBatchQueued?: boolean;
    expiredSessionID?: string;
    continueFromChatLogID?: string;
  };

  type PendingEnterText = EnterTextRetryParams & {
    msg?: string;
    waitForResponse?: {
      resolve: (event: NatsDialogResponseEvent) => void;
      reject: (error: Error) => void;
      timeoutId: ReturnType<typeof setTimeout>;
    };
  };
  const pendingEnterTextRef = useRef<Map<string, PendingEnterText>>(new Map());
  const bufferedNatsResponsesRef = useRef<Map<string, NatsDialogResponseEvent>>(
    new Map()
  );

  // Layout: from prop (string only) or integrationConfig. PII detection is only from integrationConfig (customData.layout as object with piiDetection).
  const layoutName =
    typeof layout === 'string'
      ? layout
      : typeof integrationConfig?.layout === 'string'
      ? integrationConfig.layout
      : integrationConfig?.layout?.name;
  const selectedLayout = layoutName || 'DEFAULT';
  const piiDetection: PiiDetectionConfig | undefined =
    typeof integrationConfig?.layout === 'object' &&
    integrationConfig?.layout !== null &&
    integrationConfig?.layout?.piiDetection?.enabled
      ? integrationConfig.layout.piiDetection
      : undefined;

  const defaultEnableAudio =
    enableAudio ?? integrationConfig?.enableAudio ?? true;

  const [hasUserActivatedListening, setHasUserActivatedListening] =
    useState(false);
  const [hasUserTypedMessage, setHasUserTypedMessage] = useState(false);
  const [showPositionDrawer, setShowPositionDrawer] = useState(false);
  const [showSettingsDrawer, setShowSettingsDrawer] = useState(false);
  const [showChatHistoryDrawer, setShowChatHistoryDrawer] = useState(false);
  const [showKnownFactsDrawer, setShowKnownFactsDrawer] = useState(false);
  const [showExpertsDrawer, setShowExpertsDrawer] = useState(false);
  const [continuousSpeech, setContinuousSpeech] = useState(false);
  const [continuousSpeechTimeout, setContinuousSpeechTimeout] = useState(2);
  const [controlsPosition, setControlsPosition] = useState<'center' | 'bottom'>(
    'center'
  );

  const [enablePositionControls, setEnablePositionControls] = useState(false);
  const [avatarType, setAvatarType] = useState<'blob' | 'avatar3d' | null>(
    null
  );
  const [hideEmissions, setHideEmissions] = useState(false);
  const [runtimeShowMessageConsumption, setRuntimeShowMessageConsumption] =
    useState(false);

  const speechSynthesizerRef = useRef<any | null>(null);
  const [memoriSpeaking, setMemoriSpeaking] = useState(false);

  useEffect(() => {
    setMemoriSpeaking(!!speechSynthesizerRef.current);
  }, [speechSynthesizerRef.current]);

  useEffect(() => {
    let defaultControlsPosition: 'center' | 'bottom' = 'bottom';
    let microphoneMode = getLocalConfig<string>(
      'microphoneMode',
      'HOLD_TO_TALK'
    );

    if (window.innerWidth <= 768) {
      // on mobile, default position is bottom
      defaultControlsPosition = 'bottom';
      // on mobile, keep only HOLD_TO_TALK mode
      microphoneMode = 'HOLD_TO_TALK';
    } else if (
      window.matchMedia('(orientation: portrait)').matches ||
      window.innerHeight > window.innerWidth
    ) {
      // on portrait, default position is center
      defaultControlsPosition = 'center';
    } else {
      // on landscape, default position is bottom
      defaultControlsPosition = 'bottom';
    }

    setContinuousSpeech(speakerMuted ? false : microphoneMode === 'CONTINUOUS');
    setContinuousSpeechTimeout(getLocalConfig('continuousSpeechTimeout', 2));
    setControlsPosition(
      getLocalConfig('controlsPosition', defaultControlsPosition)
    );
    setAvatarType(getLocalConfig('avatarType', 'avatar3d'));
    setHideEmissions(getLocalConfig('hideEmissions', false));
    setRuntimeShowMessageConsumption(
      getLocalConfig(
        'showMessageConsumption',
        showMessageConsumption ??
          integrationConfig?.showMessageConsumption ??
          false
      )
    );

    if (!additionalInfo?.loginToken && !authToken) {
      setLoginToken(getLocalConfig<typeof loginToken>('loginToken', undefined));
      userToken = getLocalConfig<typeof loginToken>('loginToken', undefined);

      setBirthDate(getLocalConfig<string | undefined>('birthDate', undefined));
    }

    // If audio is disabled, automatically mute the speaker
    if (!(enableAudio ?? integrationConfig?.enableAudio ?? true)) {
      setLocalConfig('muteSpeaker', true);
    }
  }, []);

  // Effect to handle enableAudio changes
  useEffect(() => {
    const isAudioEnabled =
      enableAudio ?? integrationConfig?.enableAudio ?? true;
    if (!isAudioEnabled) {
      // Force mute when audio is disabled
      setLocalConfig('muteSpeaker', true);
    }
  }, [enableAudio, integrationConfig?.enableAudio]);

  /**
   * Auth for private and secret memori
   */
  const [memoriPwd, setMemoriPwd] = useState<string | undefined>(secret);
  const [memoriTokens, setMemoriTokens] = useState<string[] | undefined>();
  const [authModalState, setAuthModalState] = useState<
    null | 'password' | 'tokens'
  >(null);

  /**
   * Position drawer
   */
  const [position, _setPosition] = useState<Venue>();

  /** True when the user has set a real position; false when position is missing or "I don't want to provide my position". */
  const hasUserProvidedPosition = useCallback((venue: Venue | undefined) => {
    if (!venue) return false;
    if (
      venue.placeName === 'Position' &&
      venue.latitude === 0 &&
      venue.longitude === 0
    ) {
      return false;
    }
    return true;
  }, []);

  /** Build optional place for EnterTextSpecs (placeName and/or lat/lon; lat/lon must be together). */
  const buildEnterTextPlace = useCallback((venue: Venue | undefined) => {
    if (!venue) return undefined;
    const place: {
      placeName?: string;
      latitude?: number;
      longitude?: number;
      uncertaintyKm?: number;
    } = {};
    if (venue.latitude != null && venue.longitude != null) {
      place.latitude = venue.latitude;
      place.longitude = venue.longitude;
      if (venue.placeName) place.placeName = venue.placeName;
      if (venue.uncertainty != null && venue.uncertainty > 0)
        place.uncertaintyKm = venue.uncertainty;
    } else if (venue.placeName) {
      place.placeName = venue.placeName;
    }
    return Object.keys(place).length > 0 ? place : undefined;
  }, []);

  /** Place to send with postEnterTextAsync: real place, nulls when no/declined position, or undefined when position not needed. */
  const getPlaceSpecForEnterText = useCallback(
    (venue: Venue | undefined) => {
      if (!memori.needsPosition) return undefined;
      return hasUserProvidedPosition(venue)
        ? buildEnterTextPlace(venue)
        : NULL_PLACE_SPEC;
    },
    [memori.needsPosition, hasUserProvidedPosition, buildEnterTextPlace]
  );

  const setPosition = (venue?: Venue) => {
    _setPosition(venue);

    // Only save position to local config if memori.needsPosition is true
    if (venue && memori.needsPosition) {
      setLocalConfig('position', JSON.stringify(venue));
    } else if (!venue) {
      removeLocalConfig('position');
    }
  };

  useEffect(() => {
    // Only load position from local config if memori.needsPosition is true
    if (memori.needsPosition) {
      const position = getLocalConfig<Venue | undefined>('position', undefined);
      if (position) {
        _setPosition(position);
      }
    }
  }, [memori.needsPosition]);

  /**
   * History e gestione invio messaggi
   */
  const [userMessage, setUserMessage] = useState<string>('');
  const onChangeUserMessage = (value: string) => {
    if (!value || value === '\n' || value.trim() === '') {
      setUserMessage('');
      // resetInteractionTimeout();
      return;
    }
    setUserMessage(value);
    clearInteractionTimeout();
  };
  const [listening, setListening] = useState(false);
  const [history, setHistory] = useState<Message[]>([]);
  const pushMessage = (message: Message) => {
    setHistory(history => [
      ...history,
      {
        ...message,
        media:
          message.media?.filter(
            m =>
              !(m.mimeType === 'text/javascript' && !!m.properties?.executable)
          ) ?? [],
      },
    ]);
  };

  // When a user resumes a chat, we need to set the chat reference link of the previous chat
  const [chatLogID, setChatLogID] = useState<string | undefined>(undefined);
  /**
   * Sends a message to the Memori and handles the response
   * @param text The text message to send
   * @param media Optional media attachments
   * @param newSessionId Optional new session ID to use
   * @param translate Whether to translate the message before sending (default true)
   * @param translatedText Optional pre-translated text
   * @param hidden Whether to hide the message from chat history (default false)
   * @param typingText Optional custom typing indicator text
   * @param useLoaderTextAsMsg Whether to use the loader text as the message (default false)
   * @param hasBatchQueued Whether there are more messages queued to be sent (default false)
   * @param skipHistoryPush Skip adding the user message to history (e.g. session-expired retry)
   */
  const sendMessage = async (
    text: string,
    media?: Medium[],
    newSessionId?: string,
    translate: boolean = true,
    translatedText?: string,
    hidden: boolean = false,
    typingText?: string,
    useLoaderTextAsMsg = false,
    hasBatchQueued = false,
    skipHistoryPush = false
  ) => {
    // Get the session ID from params or global state
    const sessionID =
      newSessionId ||
      sessionId ||
      (window.getMemoriState() as MemoriSession)?.sessionID;
    if (!sessionID || !text?.length) return;

    // Build full message text (same as what will be sent) so we can run PII check on it.
    // Order: user text -> optional translation -> appended document attachment content.
    let msg = text;
    if (
      !hidden &&
      translate &&
      isMultilanguageEnabled &&
      userLang.toUpperCase() !== language.toUpperCase()
    ) {
      const translation = await getTranslation(
        text,
        language,
        userLang,
        baseUrl
      );
      msg = translation.text;
    }
    const mediaDocuments = media?.filter(
      m => (m as any).type === 'document' && m.properties?.isAttachedFile
    );
    if (mediaDocuments && mediaDocuments.length > 0) {
      const documentContents = mediaDocuments.map(doc => doc.content).join(' ');
      msg = msg + ' ' + documentContents;
    }

    // PII check: when layout has piiDetection.enabled, run regex rules on the full msg.
    // If any rule matches, add the user message to history, then push the system error bubble and return without sending.
    if (piiDetection?.enabled) {
      const piiResult = checkPii(
        msg,
        piiDetection,
        userLang?.toLowerCase() || 'en'
      );
      if (piiResult.matched && piiResult.errorText) {
        if (!hidden) {
          pushMessage({
            text: text,
            translatedText,
            fromUser: true,
            media: media ?? [],
            initial: sessionId
              ? !!newSessionId && newSessionId !== sessionId
              : !!newSessionId,
          });
        }
        pushMessage({
          text: piiResult.errorText,
          emitter: 'system',
          fromUser: false,
          initial: false,
          contextVars: {},
          date: new Date().toISOString(),
        });
        return;
      }
    }

    // Add user message to chat history if not hidden
    if (!hidden && !skipHistoryPush)
      pushMessage({
        text: text,
        translatedText,
        fromUser: true,
        media: media ?? [],
        initial: sessionId
          ? !!newSessionId && newSessionId !== sessionId
          : !!newSessionId,
      });

    // Show typing indicator after the async enter-text request is accepted (HTTP 200).
    let gotError = false;

    try {
      const placeSpec = getPlaceSpecForEnterText(position);
      const response = await postEnterTextAsync({
        sessionId: sessionID,
        text: msg,
        ...(memori.needsDateTime && {
          dateUTC: DateTime.utc().toISO() ?? undefined,
        }),
        ...(placeSpec !== undefined && { place: placeSpec }),
      });
      const correlationID = readCorrelationID(response);
      if (response.resultCode === 0 && correlationID) {
        registerPendingEnterText(correlationID, {
          msg,
          text,
          media,
          translate,
          translatedText,
          hidden,
          typingText,
          useLoaderTextAsMsg,
          hasBatchQueued,
        });
        setMemoriTyping(true);
        setTypingText(typingText);
      } else if (response.resultCode === 0) {
        logWidgetError('enter-text missing correlationID', response);
      } else if (response.resultCode === 404) {
        retryAfterExpiredSessionRef.current({
          text,
          media,
          translate,
          translatedText,
          hidden,
          typingText,
          useLoaderTextAsMsg,
          hasBatchQueued,
          expiredSessionID: sessionID,
          continueFromChatLogID: chatLogID,
        });
      } else if (response.resultCode === 500 && response.resultMessage) {
        setHistory(h => [
          ...h,
          {
            text: 'Error: ' + response.resultMessage,
            emitter: 'system',
            fromUser: false,
            initial: false,
            contextVars: {},
            date: new Date().toISOString(),
          },
        ]);
      } else {
        return Promise.reject(response);
      }
    } catch (error) {
      gotError = true;
      logWidgetError('sendMessage failed', error);

      setTypingText(undefined);
      setMemoriTyping(false);
    }
  };

  /**
   * An enhanced version of translateDialogState that integrates smooth speaking
   * This preserves all your existing logic while improving speech reliability
   */
  const translateDialogState = async (
    state: DialogState,
    userLang: string,
    msg?: string,
    avoidPushingMessage: boolean = false
  ) => {
    const emission = state?.emission ?? currentDialogState?.emission;

    let translatedState = { ...state };
    let translatedMsg: any = null;

    // Skip translation if not needed
    if (
      !emission ||
      language.toUpperCase() === userLang.toUpperCase() ||
      !isMultilanguageEnabled ||
      avoidPushingMessage
    ) {
      translatedState = { ...state, emission };
      if (emission) {
        translatedMsg = {
          text: emission,
          emitter: state.emitter,
          media: state.emittedMedia ?? state.media,
          llmUsage: (state as any).llmUsage,
          fromUser: false,
          questionAnswered: msg,
          contextVars: state.contextVars,
          date: state.currentDate,
          placeName: state.currentPlaceName,
          placeLatitude: state.currentLatitude,
          placeLongitude: state.currentLongitude,
          placeUncertaintyKm: state.currentUncertaintyKm,
          tag: state.currentTag,
          memoryTags: state.memoryTags,
        };
      }
    } else {
      try {
        const t = await getTranslation(emission, userLang, language, baseUrl);

        // Handle hints translation if present
        if (state.hints && state.hints.length > 0) {
          const translatedHints = await Promise.all(
            (state.hints ?? []).map(async hint => {
              const tHint = await getTranslation(
                hint,
                userLang,
                language,
                baseUrl
              );
              return {
                text: tHint?.text ?? hint,
                originalText: hint,
              } as TranslatedHint;
            })
          );
          translatedState = {
            ...state,
            emission: t.text,
            translatedHints,
          };
        } else {
          translatedState = {
            ...state,
            emission: emission,
            translatedEmission: t.text,
            hints:
              state.hints ??
              (state.state === 'G1' ? currentDialogState?.hints : []),
          };
        }

        if (t.text.length > 0) {
          translatedMsg = {
            text: emission,
            translatedText: t.text,
            emitter: state.emitter,
            media: state.emittedMedia ?? state.media,
            llmUsage: (state as any).llmUsage,
            fromUser: false,
            questionAnswered: msg,
            generatedByAI: !!state.completion,
            contextVars: state.contextVars,
            date: state.currentDate,
            placeName: state.currentPlaceName,
            placeLatitude: state.currentLatitude,
            placeLongitude: state.currentLongitude,
            placeUncertaintyKm: state.currentUncertaintyKm,
            tag: state.currentTag,
            memoryTags: state.memoryTags,
          };
        }
      } catch (error) {
        translatedState = { ...state, emission };
        translatedMsg = {
          text: emission,
          emitter: state.emitter,
          media: state.emittedMedia ?? state.media,
          llmUsage: (state as any).llmUsage,
          fromUser: false,
          questionAnswered: msg,
          contextVars: state.contextVars,
          date: state.currentDate,
          placeName: state.currentPlaceName,
          placeLatitude: state.currentLatitude,
          placeLongitude: state.currentLongitude,
          placeUncertaintyKm: state.currentUncertaintyKm,
          tag: state.currentTag,
          memoryTags: state.memoryTags,
        };
      }
    }

    setCurrentDialogState(translatedState);
    if (!avoidPushingMessage && translatedMsg) {
      pushMessage(translatedMsg);
    }

    return translatedState;
  };

  /**
   * Age verification
   */
  const minAge =
    memori.ageRestriction !== undefined
      ? memori.ageRestriction
      : memori.nsfw
      ? 18
      : memori.enableCompletions
      ? 14
      : 0;
  const [birthDate, setBirthDate] = useState<string | undefined>();
  const [showAgeVerification, setShowAgeVerification] = useState(false);

  const getCultureCodeByLanguage = (lang?: string): string => {
    let voice = '';
    let voiceLang = (
      lang ||
      memori.culture?.split('-')?.[0] ||
      i18n.language ||
      'IT'
    ).toUpperCase();
    switch (voiceLang) {
      case 'IT':
        voice = 'it-IT';
        break;
      case 'DE':
        voice = 'de-DE';
        break;
      case 'EN':
        voice = 'en-GB';
        break;
      case 'ES':
        voice = 'es-ES';
        break;
      case 'FR':
        voice = 'fr-FR';
        break;
      case 'PT':
        voice = 'pt-PT';
        break;
      case 'UK':
        voice = 'uk-UK';
        break;
      case 'RU':
        voice = 'ru-RU';
        break;
      case 'PL':
        voice = 'pl-PL';
        break;
      case 'FI':
        voice = 'fi-FI';
        break;
      case 'EL':
        voice = 'el-GR';
        break;
      case 'AR':
        voice = 'ar-SA';
        break;
      case 'ZH':
        voice = 'zh-CN';
        break;
      case 'JA':
        voice = 'ja-JP';
        break;
      default:
        voice = 'it-IT';
        break;
    }
    return voice;
  };

  /**
   * Sessione
   */
  const [sessionId, setSessionId] = useState<string | undefined>(
    initialSessionID
  );
  const [currentDialogState, _setCurrentDialogState] = useState<DialogState>();
  const setCurrentDialogState = (state?: DialogState) => {
    _setCurrentDialogState(state);
    if (onStateChange) {
      onStateChange(state);
    }

    const e: MemoriNewDialogStateEvent = new CustomEvent(
      'MemoriNewDialogState',
      {
        detail: state,
      }
    );
    document.dispatchEvent(e);

    const executableSnippets = (state?.emittedMedia ?? state?.media)?.filter(
      m => m.mimeType === 'text/javascript' && !!m.properties?.executable
    );
    executableSnippets?.forEach(s => {
      try {
        setTimeout(() => {
          // eslint-disable-next-line no-new-func
          new Function(s.content ?? '')();

          setTimeout(() => {
            document
              .querySelector('.memori-chat--content')
              ?.scrollTo(
                0,
                document.querySelector('.memori-chat--content')?.scrollHeight ??
                  0
              );
          }, 400);
        }, 1000);
      } catch {
        // ignore snippet execution errors
      }
    });
  };

  useEffect(() => {
    if (initialSessionID) {
      setSessionId(initialSessionID);
      onClickStart(undefined, false, undefined, initialSessionID);
    }
  }, [initialSessionID]);

  /**
   * Opening Session
   */
  /**
   * Fetches a new session with the given parameters
   * @param params OpenSession parameters
   * @returns Promise resolving to dialog state and session ID if successful, void otherwise
   */
  const fetchSession = async (
    params: OpenSession
  ): Promise<
    | (ResponseSpec & {
        dialogState?: DialogState;
        sessionID: string;
      })
    | undefined
    | void
  > => {
    let storageBirthDate = getLocalConfig<string | undefined>(
      'birthDate',
      undefined
    );
    let userBirthDate = birthDate ?? params.birthDate ?? storageBirthDate;
    if (!userBirthDate && !!minAge) {
      setShowAgeVerification(true);
      return;
    }

    // Check if authentication is needed for private Memori
    if (
      memori.privacyType !== 'PUBLIC' &&
      !memori.secretToken &&
      !memoriPwd &&
      !memoriTokens
    ) {
      setAuthModalState('password');
      return;
    }

    if (!(await checkCredits({ notify: true }))) {
      return;
    }

    setLoading(true);

    try {
      // Check for and set giver invitation if available
      // if (!memori.giverTag && !!memori.receivedInvitations?.length) {
      //   let giverInvitation = memori.receivedInvitations.find(
      //     (i: Invitation) => i.type === 'GIVER' && i.state === 'ACCEPTED'
      //   );

      //   if (giverInvitation) {x
      //     memori.giverTag = giverInvitation.tag;
      //     memori.giverPIN = giverInvitation.pin;
      //   }
      // }

      // Get referral URL
      let referral;
      try {
        referral = (() => {
          return window.location.href;
        })();
      } catch {
        // ignore referral URL errors
      }

      // Initialize session with parameters
      const session = await initSession({
        ...params,
        birthDate: userBirthDate,
        tag: params.tag ?? personification?.tag,
        pin: params.pin ?? personification?.pin,
        additionalInfo: {
          ...(params.additionalInfo || additionalInfo || {}),
          loginToken:
            userToken ??
            loginToken ??
            params.additionalInfo?.loginToken ??
            additionalInfo?.loginToken ??
            authToken,
          language: (
            userLang ??
            memori.culture?.split('-')?.[0] ??
            'IT'
          ).toLowerCase(),
          referral: referral,
          timeZoneOffset: new Date().getTimezoneOffset().toString(),
        },
      });

      // Handle successful session creation
      if (
        session?.sessionID &&
        session?.currentState &&
        session.resultCode === 0
      ) {
        setSessionId(session.sessionID);

        // save giver state
        if (currentDialogState?.currentTag && memori.giverTag) {
          setInstruct(currentDialogState?.currentTag === memori.giverTag);
        } else {
          setInstruct(false);
        }

        setLoading(false);
        return {
          dialogState: session.currentState,
          sessionID: session.sessionID,
        } as any;
      }
      // Handle age restriction error
      else if (
        session?.resultMessage.startsWith('This Memori is aged restricted')
      ) {
        toast.error(t('underageTwinSession', { age: minAge }));
      }
      // Handle authentication error
      else if (session?.resultCode === 403 && memori.privacyType !== 'PUBLIC') {
        setMemoriPwd(undefined);
        setAuthModalState('password');
        return session;
      }
      // Handle other errors
      else {
        toast.error(
          tst => (
            <div>
              <p>{t(getErrori18nKey(session?.resultCode))}</p>
              <Button
                outlined
                padded={false}
                onClick={() => toast.dismiss(tst.id)}
                icon={<CloseIcon />}
              >
                {t('close')}
              </Button>
            </div>
          ),
          {
            duration: Infinity,
          }
        );
        return session;
      }
    } catch (err) {
      logWidgetError('fetchSession failed', err);
    }
  };

  /**
   * Reopens an existing session with optional parameters
   * @param updateDialogState Whether to update dialog state
   * @param password Optional password for authentication
   * @param recoveryTokens Optional recovery tokens
   * @param tag Optional tag
   * @param pin Optional PIN
   * @param initialContextVars Optional initial context variables
   * @param initialQuestion Optional initial question
   * @param birthDate Optional birth date for age verification
   * @returns Promise resolving to dialog state and session ID if successful, null otherwise
   */
  const reopenSession = async (
    updateDialogState: boolean = false,
    password?: string,
    recoveryTokens?: string[],
    tag?: string,
    pin?: string,
    initialContextVars?: { [key: string]: string },
    initialQuestion?: string,
    birthDate?: string,
    additionalInfoProp?: { [key: string]: string | undefined },
    continueFromChatLogID?: string,
    continueFromSessionID?: string,
    isSessionExpired?: boolean,
    suppressHistoryUpdate?: boolean
  ) => {
    // Set loading state while reopening session
    setLoading(true);

    // Get birth date from local storage if not provided
    let storageBirthDate = getLocalConfig<string | undefined>(
      'birthDate',
      undefined
    );
    let userBirthDate = birthDate ?? storageBirthDate;

    try {
      // Show age verification if required and birth date not provided
      if (!userBirthDate && !!minAge) {
        setShowAgeVerification(true);
        return;
      }

      // Check if authentication is needed based on privacy type and credentials
      if (
        memori.privacyType !== 'PUBLIC' &&
        !password &&
        !memori.secretToken &&
        !memoriPwd &&
        !recoveryTokens &&
        !memoriTokens
      ) {
        setAuthModalState('password');
        return;
      }

      if (!(await checkCredits({ notify: true }))) {
        setLoading(false);
        return null;
      }

      // Get current URL as referral
      let referral;
      try {
        referral = (() => {
          return window.location.href;
        })();
      } catch {
        // ignore referral URL errors
      }

      // Initialize session with provided parameters
      const { sessionID, currentState, ...response } = await initSession({
        memoriID: memori.engineMemoriID ?? '',
        password: password || memoriPwd || memori.secretToken,
        recoveryTokens: recoveryTokens || memoriTokens,
        tag: tag ?? personification?.tag,
        pin: pin ?? personification?.pin,
        continueFromChatLogID: continueFromChatLogID,
        continueFromSessionID: continueFromSessionID,
        initialContextVars: {
          LANG: userLang,
          PATHNAME: window.location.pathname,
          ROUTE: window.location.pathname?.split('/')?.pop() || '',
          ...(initialContextVars || {}),
        },
        initialQuestion,
        birthDate: userBirthDate,
        additionalInfo: {
          ...(additionalInfoProp || additionalInfo || {}),
          loginToken:
            userToken ??
            loginToken ??
            additionalInfoProp?.loginToken ??
            additionalInfo?.loginToken ??
            authToken,
          language: (
            userLang ??
            memori.culture?.split('-')?.[0] ??
            'IT'
          ).toLowerCase(),
          referral: referral,
          timeZoneOffset: new Date().getTimezoneOffset().toString(),
        },
      });

      // Handle successful session initialization
      if (sessionID && currentState && response.resultCode === 0) {
        setSessionId(sessionID);

        // Update dialog state and history if requested
        if (updateDialogState) {
          setCurrentDialogState(currentState);

          const sessionExpiredStatus =
            isSessionExpired && history.length > 1
              ? t('sessionExpiredReopening')
              : null;

          if (sessionExpiredStatus && suppressHistoryUpdate) {
            pushMessage({
              text: '',
              emitter: 'system',
              fromUser: false,
              initial: sessionExpiredStatus as any,
              contextVars: {},
              date: new Date().toISOString(),
            });
          }

          if (currentState.emission && !suppressHistoryUpdate) {
            // Determine initial status message based on context
            // Show status message only if session expired and there's existing history
            const initialStatus =
              sessionExpiredStatus
                ? sessionExpiredStatus
                : history.length <= 1
                ? true
                : undefined;

            // Set initial message or append to existing history
            history.length <= 1
              ? setHistory([
                  {
                    text: currentState.emission,
                    emitter: currentState.emitter,
                    media: currentState.emittedMedia ?? currentState.media,
                    fromUser: false,
                    initial: (initialStatus === true
                      ? true
                      : initialStatus || undefined) as any,
                    contextVars: currentState.contextVars,
                    date: currentState.currentDate,
                    placeName: currentState.currentPlaceName,
                    placeLatitude: currentState.currentLatitude,
                    placeLongitude: currentState.currentLongitude,
                    placeUncertaintyKm: currentState.currentUncertaintyKm,
                    tag: currentState.currentTag,
                    memoryTags: currentState.memoryTags,
                  },
                ])
              : pushMessage({
                  text: currentState.emission,
                  emitter: currentState.emitter,
                  media: currentState.emittedMedia ?? currentState.media,
                  fromUser: false,
                  initial: (initialStatus === true
                    ? true
                    : initialStatus || undefined) as any,
                  contextVars: currentState.contextVars,
                  date: currentState.currentDate,
                  placeName: currentState.currentPlaceName,
                  placeLatitude: currentState.currentLatitude,
                  placeLongitude: currentState.currentLongitude,
                  placeUncertaintyKm: currentState.currentUncertaintyKm,
                  tag: currentState.currentTag,
                  memoryTags: currentState.memoryTags,
                });
          }
        }

        setLoading(false);
        return {
          dialogState: currentState,
          sessionID,
        };
      }
      // Handle age restriction error
      else if (
        response?.resultMessage.startsWith('This Memori is aged restricted')
      ) {
        toast.error(t('underageTwinSession', { age: minAge }));
      }
      // Handle authentication error
      else if (
        response?.resultCode === 403 &&
        memori.privacyType !== 'PUBLIC'
      ) {
        setMemoriPwd(undefined);
        setAuthModalState('password');
      }
      // Handle other errors
      else {
        toast.error(t(getErrori18nKey(response.resultCode)));
      }
    } catch (err) {
      logWidgetError('reopenSession failed', err);
    }
    // Reset loading state
    setLoading(false);

    return null;
  };

  const retryAfterExpiredSessionRef = useRef<
    (params: EnterTextRetryParams) => ReturnType<typeof reopenSession>
  >(() => Promise.resolve(null));

  retryAfterExpiredSessionRef.current = (params: EnterTextRetryParams) => {
    const {
      text,
      media,
      translate = true,
      translatedText,
      hidden = false,
      typingText,
      useLoaderTextAsMsg = false,
      hasBatchQueued = false,
      expiredSessionID,
      continueFromChatLogID: continueFromChatLogIDParam,
    } = params;

    const continueFromSessionID = expiredSessionID ?? sessionId;
    const continueFromChatLogID = continueFromChatLogIDParam ?? chatLogID;

    const reopenAfterExpiry = () =>
      reopenSession(
        true,
        memoriPwd || memori.secretToken,
        memoriTokens,
        undefined,
        undefined,
        {
          LANG: userLang,
          PATHNAME: window.location.pathname,
          ROUTE: window.location.pathname?.split('/')?.pop() || '',
          ...(initialContextVars || {}),
        },
        initialQuestion,
        undefined,
        undefined,
        continueFromChatLogID,
        continueFromSessionID,
        true,
        true
      );

    const scheduleRetry = (newSessionID: string) => {
      setTimeout(() => {
        sendMessage(
          text!,
          media,
          newSessionID,
          translate,
          translatedText,
          hidden,
          typingText,
          useLoaderTextAsMsg,
          hasBatchQueued,
          true
        );
      }, 500);
    };

    const handleReopenFailure = (state: Awaited<ReturnType<typeof reopenSession>>) => {
      setMemoriTyping(false);
      setTypingText(undefined);
      // `null` = explicit failure; `undefined` = auth/age modal opened (message kept in history)
      if (state === null && text && !hidden) {
        toast.error(t('errors.SESSION_EXPIRED'));
      }
    };

    if (!text) {
      return reopenAfterExpiry().then(state => {
        if (!state?.sessionID) {
          handleReopenFailure(state);
        }
        return state;
      });
    }

    return reopenAfterExpiry().then(state => {
      if (state?.sessionID) {
        scheduleRetry(state.sessionID);
      } else {
        handleReopenFailure(state);
      }
      return state;
    });
  };

  const changeTag = async (
    memoriId: string,
    sessionId: string,
    tag?: string,
    pin?: string
  ) => {
    if (!memoriId || !sessionId) {
      return Promise.reject('Session not found');
    }

    try {
      const { currentState, resultCode } = await postTagChangedEvent(
        sessionId,
        tag ?? anonTag
      );

      if (resultCode === 0) {
        let textResult = 0;
        if (
          tag !== anonTag &&
          pin &&
          (currentState.state === 'X1a' || currentState.state === 'X1b')
        ) {
          const placeSpec = getPlaceSpecForEnterText(position);
          const { resultCode: textResultCode } = await postTextEnteredEvent({
            sessionId,
            text: pin ?? '',
            ...(memori.needsDateTime && {
              dateUTC: DateTime.utc().toISO() ?? undefined,
            }),
            ...(placeSpec !== undefined && { place: placeSpec }),
          });
          textResult = textResultCode;
        }

        if (textResult === 0) {
          const { currentState, ...response } = await getSession(sessionId);

          if (response.resultCode === 0 && !!currentState) {
            return {
              currentState,
              sessionId,
              ...response,
            };
          }
        } else if ([400, 401, 403, 404, 500].includes(resultCode)) {
          let storageBirthDate = getLocalConfig<string | undefined>(
            'birthDate',
            undefined
          );

          let referral;
          try {
            referral = (() => {
              return window.location.href;
            })();
          } catch {
            // ignore referral URL errors
          }

          fetchSession({
            memoriID: memori.engineMemoriID ?? '',
            password: secret || memoriPwd || memori.secretToken,
            tag: tag ?? personification?.tag,
            pin: pin ?? personification?.pin,
            initialContextVars: {
              LANG: userLang,
              PATHNAME: window.location.pathname,
              ROUTE: window.location.pathname?.split('/')?.pop() || '',
              ...(initialContextVars || {}),
            },
            initialQuestion,
            birthDate: birthDate || storageBirthDate || undefined,
            additionalInfo: {
              ...(additionalInfo || {}),
              loginToken:
                userToken ??
                loginToken ??
                additionalInfo?.loginToken ??
                authToken,
              language: (
                userLang ??
                memori.culture?.split('-')?.[0] ??
                'IT'
              ).toLowerCase(),
              referral: referral,
              timeZoneOffset: new Date().getTimezoneOffset().toString(),
            },
          });
        } else if (!!currentState) {
          return {
            currentState,
            sessionId,
            resultCode,
          };
        }
      }
    } catch (_e) {
      let err = _e as Error;
      return Promise.reject(err);
    }

    return null;
  };

  /**
   * Timeout conversazione
   */
  const [userInteractionTimeout, setUserInteractionTimeout] =
    useState<NodeJS.Timeout>();
  const timeoutRef = useRef<NodeJS.Timeout>();
  const clearInteractionTimeout = () => {
    if (userInteractionTimeout) {
      clearTimeout(userInteractionTimeout);
      setUserInteractionTimeout(undefined);
    }
    if (timeoutRef?.current) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = undefined;
    }
  };
  useEffect(() => {
    return () => {
      setHasUserActivatedSpeak(false);
      setClickedStart(false);
      sessionStartingRef.current = false;
      clearInteractionTimeout();
      timeoutRef.current = undefined;
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  /**
   * Speech recognition event handlers
   */
  const [requestedListening, setRequestedListening] = useState(false);
  const startListeningRef = useRef<(() => Promise<void>) | null>(null);

  // Define TTS configuration
  const ttsConfig = useMemo(
    () => ({
      provider: ttsProvider,
      voice: getTTSVoice(
        userLang || memori.culture?.split('-')?.[0] || 'EN',
        ttsProvider,
        memori.voiceType as 'MALE' | 'FEMALE' | 'NEUTRAL'
      ),
      tenant: tenantID,
      region: 'westeurope',
      voiceType: memori.voiceType,
      layout: selectedLayout,
    }),
    [ttsProvider, userLang, memori.culture, memori.voiceType, selectedLayout]
  );

  const sttConfig = useMemo(
    () => ({
      provider: ttsProvider,
      language: getCultureCodeByLanguage(userLang),
      tenant: tenantID,
    }),
    [ttsProvider, userLang]
  );

  // Initialize TTS hook with basic options first
  const {
    speak: ttsSpeak,
    stop: ttsStop,
    isPlaying: isPlayingAudio,
    speakerMuted,
    toggleMute,
    hasUserActivatedSpeak,
    setHasUserActivatedSpeak,
  } = useTTS(
    ttsConfig as TTSConfig,
    {
      apiUrl: `${baseUrl}/api/tts`,
      continuousSpeech: continuousSpeech,
      preview: preview,
    },
    autoStart,
    defaultEnableAudio,
    defaultSpeakerActive ?? integrationConfig?.defaultSpeakerActive ?? true
  );

  // Helper function to check if audio should be played.
  // When defaultEnableAudio is false, default to muted so we never play before the sync effect runs (avoids audio on first conversation start when audio is disabled).
  const shouldPlayAudio = (text?: string) => {
    const currentSpeakerMuted = getLocalConfig(
      'muteSpeaker',
      !defaultEnableAudio
    );
    return (
      text &&
      text.trim() &&
      !preview &&
      !currentSpeakerMuted &&
      defaultEnableAudio
    );
  };

  // Create a single, centralized function to process and send messages
  const processSpeechAndSendMessage = (text: string) => {
    // Skip if already processing or no text
    if (!text || text.trim().length === 0) {
      return;
    }

    try {
      // Process the text
      const message = stripDuplicates(text);

      if (message.length > 0) {
        setUserMessage('');

        // Send the message
        sendMessage(message);
      }
    } catch {
      // ignore speech processing errors
    }
  };

  const {
    isListening,

    // Actions
    startRecording,
    stopRecording,
  } = useSTT(
    sttConfig as STTConfig,
    processSpeechAndSendMessage,
    {
      apiUrl: `${baseUrl}/api/stt`,
      // continuousRecording: continuousSpeech,
      // silenceTimeout: continuousSpeechTimeout,
      // autoStart: autoStart,
    },
    defaultEnableAudio
  );

  /**
   * Enhanced handleSpeak that integrates with the improved useTTS hook
   * Uses promise-based approach for better reliability
   */
  const handleSpeak = async (text: string) => {
    if (!shouldPlayAudio(text)) {
      const e = new CustomEvent('MemoriEndSpeak');
      document.dispatchEvent(e);
      return Promise.resolve();
    }

    if (typeof stopRecording === 'function') {
      stopRecording();
    }

    // Reset the typing flag when Memori starts speaking
    setHasUserTypedMessage(false);

    const processedText = sanitizeText(text);
    return ttsSpeak(processedText);
  };
  /**
   * Integrated solution for translating dialog state and speaking
   * This uses promise chaining for reliable sequencing without timeouts
   */
  const translateAndSpeak = useCallback(
    async (
      dialogState: DialogState,
      language: string,
      msg?: string,
      skipEmission: boolean = false
    ) => {
      try {
        // First ensure we have a valid dialog state
        if (!dialogState) {
          return null;
        }

        // Then translate the dialog state
        const translatedState = await translateDialogState(
          dialogState,
          language,
          msg,
          skipEmission
        );

        // If we're not skipping emission and there's something to speak, speak it
        const textToSpeak =
          translatedState.translatedEmission || translatedState.emission;

        // Always set hasUserActivatedSpeak to true when we have a valid dialog state,
        // regardless of audio settings, so the chat can start properly
        if (!hasUserActivatedSpeak) {
          setHasUserActivatedSpeak(true);
        }

        if (textToSpeak && !skipEmission && shouldPlayAudio(textToSpeak)) {
          await handleSpeak(textToSpeak);
        }

        return translatedState;
      } catch {
        // Still update activation state even if there's an error
        if (!hasUserActivatedSpeak) {
          setHasUserActivatedSpeak(true);
        }
        return dialogState;
      }
    },
    [
      translateDialogState,
      handleSpeak,
      hasUserActivatedSpeak,
      setHasUserActivatedSpeak,
      speakerMuted,
    ]
  );

  const processEnterTextDialogResponse = useCallback(
    (event: NatsDialogResponseEvent, pending: PendingEnterText) => {
      const {
        msg,
        typingText: pendingTypingText,
        useLoaderTextAsMsg,
      } = pending;
      const currentState = event.currentState;

      if (event.resultCode !== 0 || !currentState) {
        if (isSessionExpiredNatsResponse(event) && pending.text) {
          setMemoriTyping(false);
          setTypingText(undefined);
          retryAfterExpiredSessionRef.current({
            text: pending.text,
            media: pending.media,
            translate: pending.translate,
            translatedText: pending.translatedText,
            hidden: pending.hidden,
            typingText: pending.typingText,
            useLoaderTextAsMsg: pending.useLoaderTextAsMsg,
            hasBatchQueued: pending.hasBatchQueued,
            expiredSessionID: sessionId,
            continueFromChatLogID: chatLogID,
          });
          return;
        }

        if (event.resultCode === 500 && event.resultMessage) {
          setHistory(h => [
            ...h,
            {
              text: 'Error: ' + event.resultMessage,
              emitter: 'system',
              fromUser: false,
              initial: false,
              contextVars: {},
              date: new Date().toISOString(),
            },
          ]);
        }
        return;
      }

      if (!msg) {
        return;
      }

      setChatLogID(undefined);
      const emission =
        useLoaderTextAsMsg && pendingTypingText
          ? pendingTypingText
          : currentState.emission ?? currentDialogState?.emission;

      if (
        userLang.toLowerCase() !== language.toLowerCase() &&
        emission &&
        isMultilanguageEnabled
      ) {
        currentState.emission = emission;

        translateDialogState(currentState, userLang, msg).then(ts => {
          const text = ts.translatedEmission || ts.emission;
          if (text && shouldPlayAudio(text)) {
            handleSpeak(text);
          }
        });
      } else {
        setCurrentDialogState({
          ...currentState,
          emission,
        });

        if (emission) {
          pushMessage({
            text: emission,
            emitter: currentState.emitter,
            media: currentState.emittedMedia ?? currentState.media,
            llmUsage: (currentState as any).llmUsage,
            fromUser: false,
            questionAnswered: msg,
            generatedByAI: !!currentState.completion,
            contextVars: currentState.contextVars,
            date: currentState.currentDate,
            placeName: currentState.currentPlaceName,
            placeLatitude: currentState.currentLatitude,
            placeLongitude: currentState.currentLongitude,
            placeUncertaintyKm: currentState.currentUncertaintyKm,
            tag: currentState.currentTag,
            memoryTags: currentState.memoryTags,
          } as any);
          if (emission && shouldPlayAudio(emission)) {
            handleSpeak(emission);
          }
        }
      }
    },
    [
      userLang,
      language,
      isMultilanguageEnabled,
      currentDialogState?.emission,
      translateDialogState,
      handleSpeak,
      shouldPlayAudio,
    ]
  );

  const clearEnterTextPending = useCallback(
    (correlationID: string, pending: PendingEnterText) => {
      if (pending.waitForResponse?.timeoutId) {
        clearTimeout(pending.waitForResponse.timeoutId);
      }
      pendingEnterTextRef.current.delete(correlationID);
    },
    []
  );

  const deliverEnterTextNatsError = useCallback(
    (event: NatsErrorEvent) => {
      const correlationID = event.correlationID;
      let pending: PendingEnterText | undefined;

      if (correlationID) {
        pending = pendingEnterTextRef.current.get(correlationID);
        if (pending) {
          clearEnterTextPending(correlationID, pending);
          pending.waitForResponse?.reject(
            new Error(
              event.errorMessage ?? String(event.errorCode ?? 'NATS error')
            )
          );
        }
      }

      if (isSessionExpiredNatsError(event) && pending?.text) {
        setMemoriTyping(false);
        setTypingText(undefined);
        retryAfterExpiredSessionRef.current({
          text: pending.text,
          media: pending.media,
          translate: pending.translate,
          translatedText: pending.translatedText,
          hidden: pending.hidden,
          typingText: pending.typingText,
          useLoaderTextAsMsg: pending.useLoaderTextAsMsg,
          hasBatchQueued: pending.hasBatchQueued,
          expiredSessionID: sessionId,
          continueFromChatLogID: chatLogID,
        });
        return;
      }

      const errorText = event.errorMessage
        ? `Error: ${event.errorMessage}`
        : event.errorCode
        ? `Error: ${event.errorCode}`
        : 'Error: An unexpected error occurred';

      pushMessage({
        text: errorText,
        emitter: 'system',
        fromUser: false,
        initial: false,
        contextVars: {},
        date: new Date().toISOString(),
      });

      setMemoriTyping(false);
      setTypingText(undefined);
    },
    [clearEnterTextPending]
  );

  const deliverEnterTextNatsResponse = useCallback(
    (correlationID: string, event: NatsDialogResponseEvent) => {
      const pending = pendingEnterTextRef.current.get(correlationID);
      if (!pending) {
        bufferedNatsResponsesRef.current.set(correlationID, event);
        return;
      }

      clearEnterTextPending(correlationID, pending);

      if (pending.waitForResponse) {
        pending.waitForResponse.resolve(event);
        setMemoriTyping(false);
        setTypingText(undefined);
        return;
      }

      processEnterTextDialogResponse(event, pending);

      if (!pending.hasBatchQueued) {
        setMemoriTyping(false);
        setTypingText(undefined);
      }
    },
    [processEnterTextDialogResponse, clearEnterTextPending]
  );

  const registerPendingEnterText = useCallback(
    (correlationID: string, pending: PendingEnterText) => {
      const buffered = bufferedNatsResponsesRef.current.get(correlationID);
      if (buffered) {
        bufferedNatsResponsesRef.current.delete(correlationID);
        pendingEnterTextRef.current.set(correlationID, pending);
        deliverEnterTextNatsResponse(correlationID, buffered);
        return;
      }

      pendingEnterTextRef.current.set(correlationID, pending);
    },
    [deliverEnterTextNatsResponse]
  );

  const waitForEnterTextNatsResponse = useCallback(
    (correlationID: string, timeoutMs = 120000) =>
      new Promise<NatsDialogResponseEvent>((resolve, reject) => {
        const timeoutId = setTimeout(() => {
          const current = pendingEnterTextRef.current.get(correlationID);
          if (current) {
            clearEnterTextPending(correlationID, current);
          }
          logWidgetError('NATS timeout', { correlationID, timeoutMs });
          reject(new Error('NATS enter-text response timeout'));
        }, timeoutMs);

        registerPendingEnterText(correlationID, {
          waitForResponse: {
            resolve: event => {
              clearTimeout(timeoutId);
              resolve(event);
            },
            reject: error => {
              clearTimeout(timeoutId);
              reject(error);
            },
            timeoutId,
          },
        });
      }),
    [registerPendingEnterText, clearEnterTextPending]
  );

  // NATS subscription: receives progress updates and the async enter-text response.
  useNats({
    baseUrl,
    sessionId,
    onProgress: useCallback((event: NatsProgressEvent) => {
      if (event.message) {
        setTypingText(event.message);
      }
    }, []),
    onDialogResponse: useCallback(
      (event: NatsDialogResponseEvent) => {
        const correlationID = event.correlationID;
        if (!correlationID) {
          logWidgetError('NATS dialog response missing correlationID', event);
          setMemoriTyping(false);
          setTypingText(undefined);
          return;
        }

        deliverEnterTextNatsResponse(correlationID, event);
      },
      [deliverEnterTextNatsResponse]
    ),
    onError: deliverEnterTextNatsError,
  });

  const focusChatInput = () => {
    let textarea = document.querySelector(
      '#chat-fieldset textarea'
    ) as HTMLTextAreaElement | null;
    if (textarea && enableFocusChatInput) {
      textarea.focus();
    } else {
      textarea?.blur();
    }
  };

  /**
   * Focus on the chat input on mount
   */
  useEffect(() => {
    // focus on chat input disabled for totem layout
    if (selectedLayout !== 'TOTEM') {
      focusChatInput();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [currentDialogState?.emission]);

  const resetUIEffects = () => {
    try {
      clearInteractionTimeout();
      setClickedStart(false);
      timeoutRef.current = undefined;
      ttsStop();
    } catch {
      // ignore reset errors
    }
  };
  useEffect(() => {
    return () => {
      resetUIEffects();
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);
  useEffect(() => {
    document.addEventListener('MemoriResetUIEffects', resetUIEffects);

    return () => {
      document.removeEventListener('MemoriResetUIEffects', resetUIEffects);
    };
  }, []);

  useEffect(() => {
    // if memori is speaking, don't start listening
    if (
      !isPlayingAudio &&
      continuousSpeech &&
      (hasUserActivatedListening || !requestedListening) &&
      sessionId &&
      !hasUserTypedMessage // Don't start recording if user has typed a message
    ) {
      startRecording();
    } else if (isPlayingAudio && isListening) {
      stopRecording();
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isPlayingAudio, hasUserActivatedListening, hasUserTypedMessage]);

  useEffect(() => {
    stopRecording();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [language]);

  /**
   * Textarea send mode handlers
   */
  const [sendOnEnter, setSendOnEnter] = useState<'keypress' | 'click'>(
    'keypress'
  );
  useEffect(() => {
    if (window.innerWidth <= 768 && hasTouchscreen()) setSendOnEnter('click');
    else setSendOnEnter('keypress');
  }, []);

  /**
   * Attachments
   */
  const [attachmentsMenuOpen, setAttachmentsMenuOpen] = useState<
    'link' | 'media'
  >();

  const globalBackground = integrationConfig?.globalBackground;
  const globalBackgroundUrl = globalBackground
    ? `url(${globalBackground})`
    : null;

  const integrationProperties = (
    integration
      ? {
          '--memori-chat-bubble-bg': '#fff',
          ...(integrationConfig && !instruct
            ? { '--memori-text-color': integrationConfig.textColor ?? '#000' }
            : {}),
          ...(integrationConfig?.buttonBgColor
            ? {
                '--memori-button-bg': integrationConfig.buttonBgColor,
                '--memori-primary': integrationConfig.buttonBgColor,
              }
            : {}),
          ...(integrationConfig?.buttonTextColor
            ? {
                '--memori-button-text': integrationConfig.buttonTextColor,
              }
            : {}),
          ...(integrationConfig?.blurBackground
            ? {
                '--memori-blur-background': '5px',
              }
            : {
                '--memori-blur-background': '0px',
              }),
          ...(integrationConfig?.innerBgColor
            ? {
                '--memori-inner-bg': `rgba(${
                  integrationConfig.innerBgColor === 'dark'
                    ? '0, 0, 0'
                    : '255, 255, 255'
                }, ${integrationConfig.innerBgAlpha ?? 0.4})`,
                '--memori-inner-content-pad': '1.5rem',
                '--memori-nav-bg-image': 'none',
                '--memori-nav-bg': `rgba(${
                  integrationConfig.innerBgColor === 'dark'
                    ? '0, 0, 0'
                    : '255, 255, 255'
                }, ${integrationConfig?.innerBgAlpha ?? 0.4})`,
              }
            : {
                '--memori-inner-content-pad': '0px',
              }),
        }
      : {}
  ) as CSSProperties;

  const integrationStylesheet = `
    ${
      preview ? '#preview, ' : applyVarsToRoot ? ':root, ' : ''
    }memori-client, .memori-widget, .memori-drawer, .memori-modal {
      ${Object.entries(integrationProperties)
        .map(([key, value]) => `${key}: ${value};`)
        .join('\n')}
    }
  `;

  const showAIicon =
    integrationConfig?.showAIicon === undefined
      ? true
      : integrationConfig?.showAIicon;

  const enableUpload = !!(showUpload ?? integrationConfig?.showUpload);

  const enableReasoning = !!(showReasoning ?? integrationConfig?.showReasoning);
  const enableMessageConsumption = !!runtimeShowMessageConsumption;

  const showWhyThisAnswer =
    integrationConfig?.showWhyThisAnswer === undefined
      ? true
      : integrationConfig?.showWhyThisAnswer;

  // eslint-disable-next-line
  const [avatar3dVisible, setAvatar3dVisible] = useState(false);
  useEffect(() => {
    if (
      (window.innerWidth >= 768 && selectedLayout === 'FULLPAGE') ||
      selectedLayout !== 'FULLPAGE'
    ) {
      setAvatar3dVisible(true);
    }
  }, []);

  // Put SEO tags in head
  useEffect(() => {
    if (integrationConfig?.seoTitle) {
      let meta = document.createElement('meta');
      meta.setAttribute('property', 'og:title');
      meta.setAttribute('content', integrationConfig.seoTitle);
      document.head.append(meta);
    }
    if (integrationConfig?.seoDescription) {
      let meta = document.createElement('meta');
      meta.setAttribute('property', 'og:description');
      meta.setAttribute('content', integrationConfig.seoDescription);
      document.head.append(meta);
    }
    if (integrationConfig?.seoUrl) {
      let meta = document.createElement('meta');
      meta.setAttribute('property', 'og:url');
      meta.setAttribute('content', integrationConfig.seoUrl);
      document.head.append(meta);
    }
    let image = ogImage || memori.avatarURL;
    if (integrationConfig?.seoImageShowAvatar && image) {
      let meta = document.createElement('meta');
      meta.setAttribute('property', 'og:image');
      meta.setAttribute('content', image);
      document.head.append(meta);
    }
  }, [integrationConfig, memori.avatarURL, ogImage]);

  const simulateUserPrompt = (text: string, translatedText?: string) => {
    ttsStop();
    sendMessage(text, undefined, undefined, false, translatedText);
  };

  // listen to events from browser
  // to use in integrations or snippets
  const memoriTextEnteredHandler = useCallback(
    (e: MemoriTextEnteredEvent) => {
      if (disableTextEnteredEvents) {
        return;
      }

      const {
        text,
        waitForPrevious,
        hidden,
        typingText,
        useLoaderTextAsMsg,
        hasBatchQueued,
      } = e.detail;

      if (text) {
        // wait to finish reading previous emission
        if (
          waitForPrevious &&
          !speakerMuted &&
          (memoriSpeaking || !!memoriTyping)
        ) {
          setTimeout(() => {
            memoriTextEnteredHandler(e);
          }, 1000);
        } else {
          ttsStop();
          sendMessage(
            text,
            undefined,
            undefined,
            undefined,
            undefined,
            hidden,
            typingText,
            useLoaderTextAsMsg,
            hasBatchQueued
          );
        }
      }
    },
    [
      sessionId,
      isPlayingAudio,
      memoriTyping,
      userLang,
      disableTextEnteredEvents,
      speakerMuted,
    ]
  );
  useEffect(() => {
    if (!disableTextEnteredEvents) {
      document.addEventListener('MemoriTextEntered', memoriTextEnteredHandler);
    } else {
      document.removeEventListener(
        'MemoriTextEntered',
        memoriTextEnteredHandler
      );
    }

    return () => {
      document.removeEventListener(
        'MemoriTextEntered',
        memoriTextEnteredHandler
      );
    };
  }, [sessionId, userLang, disableTextEnteredEvents]);

  /**
   * Handles clicking the start button to begin or resume a session
   * @param session Optional existing session with dialog state and ID
   * @param initialSessionExpired Whether the initial session has expired
   /**
    * Handles clicking the start button to begin or resume a session
    * @param session Optional existing session with dialog state and ID
    * @param initialSessionExpired Whether the initial session has expired
    */
  const onClickStart = useCallback(
    async (
      session?: { dialogState: DialogState; sessionID: string },
      initialSessionExpired = false,
      chatLog?: ChatLog,
      targetSessionID?: string
    ) => {
      const sessionID = chatLog ? undefined : session?.sessionID || sessionId;
      const dialogState = chatLog
        ? undefined
        : session?.dialogState || currentDialogState;
      setClickedStart(true);
      setHasUserTypedMessage(false); // Reset typing flag when starting a new session

      let translatedMessages: Message[] = [];

      // Get birth date from storage or props
      let storageBirthDate = getLocalConfig<string | undefined>(
        'birthDate',
        undefined
      );
      let birth = birthDate || storageBirthDate || user?.birthDate;
      if (!birth && autoStart && (initialSessionID || targetSessionID))
        birth = '1970-01-01T10:24:03.845Z';

      const localPosition = getLocalConfig<Venue | undefined>(
        'position',
        undefined
      );
      // Only check for position requirement if memori.needsPosition is true
      if (autoStart && !localPosition && memori.needsPosition) {
        setShowPositionDrawer(true);
        return;
      }

      if (!(await checkCredits({ notify: true }))) {
        setClickedStart(false);
        setLoading(false);
        return;
      }

      // Handle age verification
      if (!sessionID && !!minAge && !birth) {
        setShowAgeVerification(true);
        setClickedStart(false);
        return;
      }
      // Handle authentication
      else if (
        !sessionID &&
        memori.privacyType !== 'PUBLIC' &&
        !memori.secretToken &&
        !memoriPwd &&
        !memoriTokens
      ) {
        setAuthModalState('password');
        setClickedStart(false);
        return;
      }
      // Create new session if needed
      else if (!sessionID || initialSessionExpired) {
        if (sessionStartingRef.current) {
          return;
        }
        sessionStartingRef.current = true;
        try {
          const session = await fetchSession({
            memoriID: memori.engineMemoriID!,
            password: secret || memoriPwd || memori.secretToken,
            tag: personification?.tag,
            pin: personification?.pin,
            continueFromChatLogID: chatLog?.chatLogID,
            initialContextVars: {
              LANG: userLang,
              PATHNAME: window.location.pathname?.toUpperCase(),
              ROUTE:
                window.location.pathname?.split('/')?.pop()?.toUpperCase() ||
                '',
              ...((!chatLog
                ? initialContextVars
                : chatLog.lines[chatLog.lines.length - 1].contextVars) || {}),
            },
            initialQuestion: chatLog ? undefined : initialQuestion,
            birthDate: birth,
            additionalInfo: {
              ...(additionalInfo || {}),
              loginToken:
                userToken ??
                loginToken ??
                additionalInfo?.loginToken ??
                authToken,
              language: (
                userLang ??
                memori.culture?.split('-')?.[0] ??
                'IT'
              ).toLowerCase(),
              timeZoneOffset: new Date().getTimezoneOffset().toString(),
            },
          });

          if (session?.dialogState) {
          // reset history
          if (!chatLog) {
            setHistory([]);

            // Use translateAndSpeak which already handles the speaking
            await translateAndSpeak(session.dialogState, userLang);
            // No need for additional handleSpeak call since translateAndSpeak already handles it
            setHasUserActivatedSpeak(true);
            setClickedStart(false);
          } else {
            const messages = chatLog.lines.map(
              (l, i) =>
                ({
                  text: l.text,
                  media: l.media
                    ?.filter(m => allowedMediaTypes.includes(m.mimeType))
                    ?.map(m => ({
                      mediumID: `${i}-${m.mimeType}`,
                      ...m,
                    })),
                  fromUser: l.inbound,
                  llmUsage: (l as any).llmUsage,
                  timestamp: l.timestamp,
                  emitter: l.emitter,
                  initial: i === 0,
                } as Message)
            );

            // we dont remove the last one as it is the current state
            translatedMessages = messages ?? [];
            if (
              language.toUpperCase() !== userLang.toUpperCase() &&
              isMultilanguageEnabled
            ) {
              try {
                translatedMessages = await Promise.all(
                  messages.map(async m => {
                    // If original text is present, the message is already translated
                    if ('originalText' in m && m.originalText) {
                      return m;
                    }
                    // Otherwise translate the message
                    return {
                      ...m,
                      originalText: m.text,
                      text: (
                        await getTranslation(
                          m.text,
                          userLang,
                          language,
                          baseUrl
                        )
                      ).text,
                    };
                  })
                );
              } catch {
                // ignore translation errors
              }
            }

            setHistory(translatedMessages);

            translateDialogState(
              session.dialogState,
              userLang,
              undefined,
              true
            ).finally(() => {
              setHasUserActivatedSpeak(true);
              setClickedStart(false);
            });
          }
          } else if (session?.resultCode === 0) {
            sessionStartingRef.current = false;
            await onClickStart((session as any) || undefined);
          } else {
            setLoading(false);
            setClickedStart(false);
          }
        } finally {
          sessionStartingRef.current = false;
        }

        return;
      }
      // Handle initial session
      else if (initialSessionID || targetSessionID) {
        const sessionID = targetSessionID ?? initialSessionID;
        // check if session is valid and not expired
        const { currentState, ...response } = await getSession(sessionID!);

        if (response.resultCode !== 0 || !currentState) {
          const { chatLogs } = await getSessionChatLogs(sessionID!, sessionID!);
          setSessionId(undefined);
          await onClickStart(undefined, true, chatLogs?.[0]);
          return;
        }

        // reset history
        setHistory([]);

        // Handle personification tag changes
        if (
          personification &&
          currentState.currentTag !== personification.tag
        ) {
          try {
            // reset tag
            await changeTag(memori.engineMemoriID!, sessionID!, '-');
            // change tag to receiver
            const session = await changeTag(
              memori.engineMemoriID!,
              sessionID!,
              personification.tag,
              personification.pin
            );

            if (session && session.resultCode === 0) {
              await translateAndSpeak(session.currentState, userLang);
              setClickedStart(false);
            } else {
              throw new Error('No session');
            }
          } catch {
            reopenSession(
              true,
              memori?.secretToken,
              undefined,
              personification.tag,
              personification.pin,
              {
                LANG: userLang,
                PATHNAME: window.location.pathname?.toUpperCase(),
                ROUTE:
                  window.location.pathname?.split('/')?.pop()?.toUpperCase() ||
                  '',
                ...(initialContextVars || {}),
              },
              initialQuestion,
              birth
            ).then(() => {
              setHasUserActivatedSpeak(true);
              setClickedStart(false);
            });
          }
        }
        // Handle anonymous tag changes
        else if (
          !personification &&
          currentState?.currentTag &&
          currentState?.currentTag !== anonTag &&
          currentState?.currentTag !== '-'
        ) {
          try {
            // reset tag
            await changeTag(memori.engineMemoriID!, sessionID!, '-');
            // change tag to anonymous
            const session = await changeTag(
              memori.engineMemoriID!,
              sessionID!,
              anonTag
            );

            if (session && session.resultCode === 0) {
              await translateAndSpeak(session.currentState, userLang);
              setClickedStart(false);
            } else {
              throw new Error('No session');
            }
          } catch (e) {
            reopenSession(
              true,
              memori?.secretToken,
              undefined,
              undefined,
              undefined,
              {
                LANG: userLang,
                PATHNAME: window.location.pathname?.toUpperCase(),
                ROUTE:
                  window.location.pathname?.split('/')?.pop()?.toUpperCase() ||
                  '',
                ...(initialContextVars || {}),
              },
              initialQuestion,
              birth
            ).then(() => {
              setHasUserActivatedSpeak(true);
              setClickedStart(false);
            });
          }
        }
        // No tag changes needed
        else {
          try {
            //This is the session id of the session that was opened before the current session
            const { chatLogs } = await getSessionChatLogs(
              sessionID!,
              sessionID!
            );

            const messages = chatLogs?.[0]?.lines.map(
              (l, i) =>
                ({
                  text: l.text,
                  media: l.media
                    ?.filter(m => allowedMediaTypes.includes(m.mimeType))
                    ?.map(m => ({
                      mediumID: `${i}-${m.mimeType}`,
                      ...m,
                    })),
                  fromUser: l.inbound,
                  llmUsage: (l as any).llmUsage,
                  timestamp: l.timestamp,
                  emitter: l.emitter,
                  initial: i === 0,
                } as Message)
            );

            // we dont remove the last one as it is the current state
            translatedMessages = messages ?? [];
            if (
              language.toUpperCase() !== userLang.toUpperCase() &&
              isMultilanguageEnabled
            ) {
              try {
                translatedMessages = await Promise.all(
                  messages.map(async m => ({
                    ...m,
                    originalText: m.text,
                    text: (
                      await getTranslation(m.text, userLang, language, baseUrl)
                    ).text,
                  }))
                );
              } catch {
                // ignore translation errors
              }
            }

            setHistory(translatedMessages);
          } catch {
            // ignore chat log retrieval errors
          }

          if (
            (!!translatedMessages?.length && translatedMessages.length > 1) ||
            !initialQuestion
          ) {
            // we have a history, don't push message
            setHasUserActivatedSpeak(true);
            setClickedStart(false);
            await translateAndSpeak(
              currentState,
              userLang,
              undefined,
              // if empty history, pick current state emission
              // otherwise, don't push message
              !!translatedMessages?.length
            );
          } else {
            // remove default initial message
            translatedMessages = [];
            setHistory([]);

            // we have no chat history, we start by initial question
            const placeSpec = getPlaceSpecForEnterText(position);
            const response = await postEnterTextAsync({
              sessionId: sessionID!,
              text: initialQuestion,
              ...(memori.needsDateTime && {
                dateUTC: DateTime.utc().toISO() ?? undefined,
              }),
              ...(placeSpec !== undefined && { place: placeSpec }),
            });
            // Handle 500 error from EnterTextAsync
            if (response.resultCode === 500 && response.resultMessage) {
              setHistory(h => [
                ...h,
                {
                  text: 'Error: ' + response.resultMessage,
                  emitter: 'system',
                  fromUser: false,
                  initial: false,
                  contextVars: {},
                  date: new Date().toISOString(),
                },
              ]);
              return;
            }

            const onClickStartCorrelationID = readCorrelationID(response);
            if (response.resultCode === 0 && onClickStartCorrelationID) {
              setMemoriTyping(true);
              try {
                const natsEvent = await waitForEnterTextNatsResponse(
                  onClickStartCorrelationID
                );
                if (natsEvent.resultCode === 0 && natsEvent.currentState) {
                  await translateAndSpeak(
                    natsEvent.currentState,
                    userLang,
                    undefined,
                    false
                  );
                  setClickedStart(false);
                }
              } catch (err) {
                logWidgetError('onClickStart NATS wait failed', err);
                setMemoriTyping(false);
                setTypingText(undefined);
              }
            } else if (response.resultCode === 0) {
              logWidgetError('onClickStart enter-text missing correlationID', response);
            }
          }
        }
      }
      // Default case - just translate and activate
      else {
        // reset history
        setHistory([]);

        // everything is fine, just translate dialog state and activate chat
        await translateAndSpeak(dialogState!, userLang);
        setClickedStart(false);
      }
    },
    [memoriPwd, memori, memoriTokens, birthDate, sessionId, userLang, position]
  );

  // check if owner has enough credits
  const needsCredits = tenant?.billingDelegation;
  const [hasEnoughCredits, setHasEnoughCredits] = useState<boolean>(true);

  useEffect(() => {
    if (
      !clickedStart &&
      !sessionStartingRef.current &&
      !sessionId &&
      autoStart &&
      selectedLayout !== 'HIDDEN_CHAT' &&
      (!needsCredits || hasEnoughCredits)
    ) {
      onClickStart();
    }
  }, [
    clickedStart,
    autoStart,
    selectedLayout,
    sessionId,
    needsCredits,
    hasEnoughCredits,
  ]);

  useEffect(() => {
    const targetNode =
      document.querySelector(`memori-client[memoriname="${memori.name}"]`) ||
      document.querySelector(`memori-client[memoriid="${memori.memoriID}"]`) ||
      document.querySelector('memori-client');
    if (!targetNode) {
      return;
    }

    const config = { attributes: true, childList: false, subtree: false };
    const callback: MutationCallback = (mutationList, _observer) => {
      for (const mutation of mutationList) {
        if (
          mutation.type === 'attributes' &&
          mutation.attributeName?.toLowerCase() === 'authtoken'
        ) {
          if (mutation.target.nodeName === 'MEMORI-CLIENT') {
            setLoginToken(
              // @ts-ignore
              mutation.target.getAttribute('authtoken') || undefined
            );
            // @ts-ignore
            userToken = mutation.target.getAttribute('authtoken') || undefined;
          } else {
            // @ts-ignore
            setLoginToken(
              mutation.target?.parentElement?.getAttribute('authtoken') ||
                undefined
            );
            // @ts-ignore
            userToken = mutation.target.getAttribute('authtoken') || undefined;
          }
        }
      }
    };
    const observer = new MutationObserver(callback);
    observer.observe(targetNode, config);

    return () => {
      observer.disconnect();
    };
  }, []);

  /**
   * Experts references
   */
  const [experts, setExperts] = useState<ExpertReference[]>();
  const fetchExperts = useCallback(async () => {
    if (!sessionId || !memori?.enableBoardOfExperts) return;

    try {
      const { experts, count, ...resp } = await getExpertReferences(sessionId);

      if (resp.resultCode === 0) {
        setExperts(experts);
      }
    } catch {
      // ignore expert fetch errors
    }
  }, [sessionId, memori?.enableBoardOfExperts]);
  useEffect(() => {
    fetchExperts();
  }, [sessionId, fetchExperts]);

  const deepThoughtEnabled =
    memori.enableDeepThought &&
    !!loginToken &&
    !!user?.userID &&
    user?.pAndCUAccepted;

  const handleNotEnoughCredits = useCallback(() => {
    setHasEnoughCredits(false);
    setAuthModalState(null);
    toast.error(t('notEnoughCredits'));
  }, [t]);
  const checkCredits = useCallback(
    async (options?: { notify?: boolean }) => {
      if (!tenant?.billingDelegation) return true;

      // Billing delegation is active: credits MUST be verified.
      // Without either owner identifier we cannot call the API, so we fail closed
      // instead of silently letting the session start unverified.
      if (!ownerUserID && !ownerUserName) {
        if (options?.notify) {
          handleNotEnoughCredits();
        } else {
          setHasEnoughCredits(false);
        }
        return false;
      }

      try {
        const resp = await getCredits({
          operation: deepThoughtEnabled
            ? 'dt_session_creation'
            : 'session_creation',
          baseUrl: baseUrl,
          userID: ownerUserID,
          userName: ownerUserName,
          tenant: tenantID,
        });

        if (resp.enough) {
          setHasEnoughCredits(true);
          return true;
        } else {
          if (options?.notify) {
            handleNotEnoughCredits();
          } else {
            setHasEnoughCredits(false);
          }
          return false;
        }
      } catch (err) {
        logWidgetError('checkCredits failed', err);
        return true;
      }
    },
    [
      baseUrl,
      deepThoughtEnabled,
      handleNotEnoughCredits,
      ownerUserID,
      ownerUserName,
      tenant?.billingDelegation,
      tenantID,
    ]
  );
  useEffect(() => {
    if (tenant?.billingDelegation) {
      checkCredits();
    }
  }, [tenant?.billingDelegation, deepThoughtEnabled, checkCredits]);

  useEffect(() => {
    if (__WEBCOMPONENT__) return;

    const closeSession = () => {
      if (sessionId) {
        deleteSession(sessionId);
      }
    };

    // delete session when the user closes the browser tab
    window.addEventListener('beforeunload', closeSession);

    return () => {
      window.removeEventListener('beforeunload', closeSession);
      closeSession();
    };
  }, [sessionId]);

  const showFullHistory =
    showOnlyLastMessages === undefined
      ? selectedLayout !== 'TOTEM' && selectedLayout !== 'WEBSITE_ASSISTANT'
      : !showOnlyLastMessages;
  const canShowLoginButton =
    !tenant?.ssoLogin &&
    (showLogin ?? integrationConfig?.showLogin ?? memori.requireLoginToken);

  const headerProps: HeaderProps = {
    memori: {
      ...memori,
      ownerUserID: memori.ownerUserID ?? ownerUserID ?? undefined,
    },
    apiClient: client,
    tenant,
    history,
    showShare: showShare ?? integrationConfig?.showShare ?? true,
    position,
    layout: selectedLayout,
    additionalSettings,
    setShowPositionDrawer,
    setShowSettingsDrawer,
    setShowKnownFactsDrawer,
    setShowExpertsDrawer,
    enableAudio: enableAudio ?? integrationConfig?.enableAudio ?? true,
    speakerMuted: speakerMuted ?? false,
    setSpeakerMuted: (mute: boolean) => {
      toggleMute(mute);
    },
    setShowChatHistoryDrawer,
    showSettings: showSettings ?? integrationConfig?.showSettings ?? true,
    showChatHistory:
      showChatHistory ?? integrationConfig?.showChatHistory ?? true,
    showMessageConsumption: enableMessageConsumption,
    hasUserActivatedSpeak,
    showReload: selectedLayout === 'TOTEM',
    showClear: showClear ?? integrationConfig?.showClear ?? false,
    clearHistory: () => setHistory(h => h.slice(-1)),
    showLogin: canShowLoginButton,
    setShowLoginDrawer,
    loginToken,
    user,
    sessionID: sessionId,
    baseUrl,
    onLogout: () => {
      if (!loginToken) return;

      client.backend.pwlUserLogout(loginToken).then(() => {
        setShowLoginDrawer(false);
        setUser(undefined);
        setLoginToken(undefined);
        userToken = undefined;
        removeLocalConfig('loginToken');
      });
    },
  };

  const avatarProps: AvatarProps = {
    memori,
    integration,
    integrationConfig,
    tenant,
    instruct,
    avatar3dVisible,
    setAvatar3dVisible,
    hasUserActivatedSpeak,
    isPlayingAudio:
      isPlayingAudio &&
      !speakerMuted &&
      (enableAudio ?? integrationConfig?.enableAudio ?? true),
    loading: !!memoriTyping,
    baseUrl,
    apiUrl: client.constants.BACKEND_URL,
    enablePositionControls,
    setEnablePositionControls,
    avatarType,
  };

  const startPanelProps: StartPanelProps = {
    memori,
    tenant: tenant,
    language: language,
    userLang: userLang,
    setUserLang: setUserLang,
    baseUrl: baseUrl,
    apiUrl: client.constants.BACKEND_URL,
    position: position,
    openPositionDrawer: () => setShowPositionDrawer(true),
    integrationConfig: integrationConfig,
    instruct: instruct,
    sessionId: sessionId,
    clickedStart: clickedStart,
    isMultilanguageEnabled: isMultilanguageEnabled,
    onClickStart: onClickStart,
    isUserLoggedIn: !!loginToken && !!user?.userID,
    hasInitialSession: !!initialSessionID,
    notEnoughCredits: needsCredits && !hasEnoughCredits,
    showLogin: canShowLoginButton,
    setShowLoginDrawer,
    user,
  };

  const chatProps: ChatProps = {
    memori,
    sessionID: sessionId || '',
    tenant,
    translateTo:
      isMultilanguageEnabled &&
      userLang.toUpperCase() !==
        (
          memori.culture?.split('-')?.[0] ??
          i18n.language ??
          'IT'
        )?.toUpperCase()
        ? userLang
        : undefined,
    baseUrl,
    apiUrl: client.constants.BACKEND_URL,
    layout,
    memoriTyping,
    typingText,
    showTypingText:
      showTypingText ?? integrationConfig?.showTypingText ?? false,
    history: showFullHistory ? history : history.slice(-2),
    authToken:
      loginToken ?? userToken ?? additionalInfo?.loginToken ?? authToken,
    dialogState: currentDialogState,
    pushMessage,
    simulateUserPrompt,
    showDates,
    showContextPerLine,
    showMessageConsumption: enableMessageConsumption,
    showAIicon,
    showUpload: enableUpload,
    showReasoning: enableReasoning,
    showWhyThisAnswer,
    showCopyButton: showCopyButton ?? integrationConfig?.showCopyButton ?? true,
    showTranslationOriginal:
      showTranslationOriginal ??
      integrationConfig?.showTranslationOriginal ??
      false,
    client,
    instruct,
    preview,
    sendOnEnter,
    setSendOnEnter,
    microphoneMode: continuousSpeech ? 'CONTINUOUS' : 'HOLD_TO_TALK',
    attachmentsMenuOpen,
    setAttachmentsMenuOpen,
    showInputs,
    showMicrophone:
      !!ttsProvider && (enableAudio ?? integrationConfig?.enableAudio ?? true),
    showFunctionCache,
    userMessage,
    onChangeUserMessage,
    sendMessage: (msg: string, media?: (Medium & { type: string })[]) => {
      ttsStop();
      stopRecording();
      setHasUserTypedMessage(true); // Mark that user has typed a message
      sendMessage(msg, media);
      setUserMessage('');
    },
    stopListening: stopRecording,
    startListening: () => {
      setHasUserTypedMessage(false); // Reset typing flag when user starts listening
      startRecording();
    },
    stopAudio: ttsStop,
    listening: isListening,
    setEnableFocusChatInput,
    isPlayingAudio,
    customMediaRenderer,
    user,
    userAvatar,
    experts,
    useMathFormatting: applyMathFormatting,
    maxTotalMessagePayload,
    maxTextareaCharacters,
  };

  const integrationBackground =
    integration && globalBackgroundUrl ? (
      <div className="memori--global-background">
        <div
          className="memori--global-background-image"
          style={{ backgroundImage: globalBackgroundUrl }}
        />
      </div>
    ) : (
      <div className="memori--global-background no-background-image" />
    );

  const integrationStyle = integration ? (
    <style dangerouslySetInnerHTML={{ __html: integrationStylesheet }} />
  ) : null;

  const poweredBy = (
    <PoweredBy
      tenant={tenant}
      userLang={userLang}
      integrationID={integration?.integrationID}
      memoriHash={`${memori.ownerTenantName}-${memori.ownerUserName}-${memori.name}`}
    />
  );

  const Layout = customLayout
    ? customLayout
    : selectedLayout === 'TOTEM'
    ? TotemLayout
    : selectedLayout === 'CHAT'
    ? ChatLayout
    : selectedLayout === 'FULLPAGE'
    ? FullPageLayout
    : selectedLayout === 'WEBSITE_ASSISTANT'
    ? WebsiteAssistantLayout
    : selectedLayout === 'HIDDEN_CHAT'
    ? HiddenChatLayout
    : selectedLayout === 'ZOOMED_FULL_BODY'
    ? ZoomedFullBodyLayout
    : FullPageLayout;

  return (
    <div
      className={cx(
        'memori',
        'memori-widget',
        `memori-layout-${selectedLayout.toLowerCase()}`,
        `memori-controls-${controlsPosition.toLowerCase()}`,
        `memori--avatar-${integrationConfig?.avatar || 'default'}`,
        {
          'memori--auto-start': autoStart,
          'memori--preview': preview,
          'memori--embed': embed,
          'memori--with-integration': integration,
          'memori--with-speechkey': !!ttsProvider,
          'memori--active': hasUserActivatedSpeak,
          'memori--hide-emissions': hideEmissions,
          'memori--has-active-session': !!sessionId,
        }
      )}
      data-memori-name={memori?.name}
      data-memori-id={memori?.engineMemoriID}
      data-memori-secondary-id={memori?.memoriID}
      data-memori-session-id={sessionId}
      data-memori-integration={integration?.integrationID}
      data-memori-engine-state={JSON.stringify({
        ...currentDialogState,
        sessionID: sessionId,
      })}
      style={{ height }}
    >
      <Layout
        Header={Header}
        headerProps={headerProps}
        Avatar={Avatar}
        avatarProps={avatarProps}
        Chat={Chat}
        chatProps={chatProps}
        StartPanel={StartPanel}
        startPanelProps={startPanelProps}
        integrationStyle={integrationStyle}
        integrationBackground={integrationBackground}
        poweredBy={poweredBy}
        autoStart={autoStart}
        sessionId={sessionId}
        hasUserActivatedSpeak={hasUserActivatedSpeak}
        loading={loading}
        avatar3dHidden={avatar3dHidden ?? integrationConfig?.avatar_3d_hidden}
      />

      <ArtifactAPIBridge
        pushMessage={(message: Message) => {
          setHistory(history => {
            if (!history.length) return history;
            const lastMessage = history[history.length - 1];
            if (!lastMessage || lastMessage.fromUser) return history;
            // Create a new message object with the updated text
            const updatedLastMessage = {
              ...lastMessage,
              text: lastMessage.text + message.text,
            };
            return [...history.slice(0, -1), updatedLastMessage];
          });
        }}
      />

      <audio
        id="memori-audio"
        style={{ display: 'none' }}
        src="https://aisuru.com/intro.mp3"
      />

      {isClient && (
        <MemoriAuth
          withModal
          pwdOrTokens={authModalState}
          openModal={!!authModalState}
          setPwdOrTokens={setAuthModalState}
          showTokens={memori.privacyType === 'SECRET'}
          onFinish={(values: any) => {
            if (values['password']) setMemoriPwd(values['password']);
            if (values['password']) memoriPassword = values['password'];
            if (values['tokens']) setMemoriTokens(values['tokens']);

            return reopenSession(
              !sessionId,
              values['password'],
              values['tokens'],
              personification?.tag,
              personification?.pin,
              {
                LANG: userLang,
                PATHNAME: window.location.pathname?.toUpperCase(),
                ROUTE:
                  window.location.pathname?.split('/')?.pop()?.toUpperCase() ||
                  '',
                ...(initialContextVars || {}),
              },
              initialQuestion,
              birthDate
            )
              .then(state => {
                if (!state?.sessionID) {
                  throw new Error('AUTH_FAILED');
                }

                setAuthModalState(null);
                // If we got a valid state from reopenSession, don't call onClickStart again
                // to avoid duplicate snippet execution
                if (state?.dialogState) {
                  setHasUserActivatedSpeak(true);
                } else {
                  // Only call onClickStart if reopenSession didn't return a valid state
                  onClickStart(state);
                }
              })
              .catch(error => {
                throw error;
              });
          }}
          minimumNumberOfRecoveryTokens={
            memori?.minimumNumberOfRecoveryTokens ?? 1
          }
        />
      )}

      {isClient && (
        <AgeVerificationModal
          visible={showAgeVerification}
          minAge={minAge}
          onClose={birthDate => {
            if (birthDate) {
              setBirthDate(birthDate);

              setLocalConfig('birthDate', birthDate);

              reopenSession(
                !sessionId,
                memoriPassword || memoriPwd || memori?.secretToken,
                memoriTokens,
                personification?.tag,
                personification?.pin,
                {
                  LANG: userLang,
                  PATHNAME: window.location.pathname?.toUpperCase(),
                  ROUTE:
                    window.location.pathname
                      ?.split('/')
                      ?.pop()
                      ?.toUpperCase() || '',
                  ...(initialContextVars || {}),
                },
                initialQuestion,
                birthDate
              )
                .then(state => {
                  setShowAgeVerification(false);
                  setAuthModalState(null);
                  onClickStart(state || undefined);
                })
                .catch(() => {
                  setShowAgeVerification(false);
                });
            } else {
              setShowAgeVerification(false);
              setClickedStart(false);
            }
          }}
        />
      )}

      {showSettingsDrawer && (
        <SettingsDrawer
          layout={selectedLayout}
          open={!!showSettingsDrawer}
          onClose={() => setShowSettingsDrawer(false)}
          microphoneMode={continuousSpeech ? 'CONTINUOUS' : 'HOLD_TO_TALK'}
          continuousSpeechTimeout={continuousSpeechTimeout}
          setMicrophoneMode={mode => setContinuousSpeech(mode === 'CONTINUOUS')}
          setContinuousSpeechTimeout={setContinuousSpeechTimeout}
          controlsPosition={controlsPosition}
          setControlsPosition={setControlsPosition}
          hideEmissions={hideEmissions}
          setHideEmissions={setHideEmissions}
          avatarType={avatarType}
          setAvatarType={setAvatarType}
          enablePositionControls={enablePositionControls}
          setEnablePositionControls={setEnablePositionControls}
          isAvatar3d={!!integrationConfig?.avatarURL}
          additionalSettings={additionalSettings}
          speakerMuted={speakerMuted}
        />
      )}

      {showChatHistoryDrawer && (
        <ChatHistoryDrawer
          open={!!showChatHistoryDrawer}
          onClose={() => setShowChatHistoryDrawer(false)}
          resumeSession={chatLog => {
            setChatLogID(chatLog.chatLogID);
            onClickStart(undefined, false, chatLog);
            setShowChatHistoryDrawer(false);
          }}
          apiClient={client}
          sessionId={sessionId || ''}
          memori={memori}
          baseUrl={baseUrl}
          history={history}
          apiUrl={client.constants.BACKEND_URL}
          loginToken={loginToken}
          language={language}
          userLang={userLang}
          isMultilanguageEnabled={isMultilanguageEnabled}
        />
      )}

      {showPositionDrawer && (
        <PositionDrawer
          memori={memori}
          open={!!showPositionDrawer}
          venue={position}
          setVenue={setPosition}
          onClose={() => {
            setShowPositionDrawer(false);
            if (autoStart) {
              onClickStart();
            }
          }}
          drawerClassName={
            selectedLayout === 'WEBSITE_ASSISTANT'
              ? 'memori-drawer--above-website-assistant'
              : undefined
          }
        />
      )}

      {showKnownFactsDrawer && sessionId && (
        <KnownFacts
          apiClient={client}
          memori={memori}
          sessionID={sessionId}
          visible={showKnownFactsDrawer}
          closeDrawer={() => setShowKnownFactsDrawer(false)}
        />
      )}

      {showExpertsDrawer && !!experts && (
        <ExpertsDrawer
          apiUrl={client.constants.BACKEND_URL}
          baseUrl={baseUrl}
          tenant={tenant}
          experts={experts}
          open={showExpertsDrawer}
          onClose={() => setShowExpertsDrawer(false)}
        />
      )}

      {showLoginDrawer && tenant?.name && (
        <LoginDrawer
          tenant={tenant}
          apiClient={client}
          open={!!showLoginDrawer}
          user={user}
          loginToken={loginToken}
          onClose={() => setShowLoginDrawer(false)}
          drawerClassName={
            selectedLayout === 'WEBSITE_ASSISTANT'
              ? 'memori-drawer--above-website-assistant'
              : undefined
          }
          onLogin={(user, token) => {
            //The user is logged in, so we need to set open a new session with the new token
            reopenSession(
              false,
              memoriPassword || memoriPwd || memori?.secretToken,
              [],
              personification?.tag,
              personification?.pin,
              {
                LANG: userLang,
                PATHNAME: window.location.pathname?.toUpperCase(),
                ROUTE:
                  window.location.pathname?.split('/')?.pop()?.toUpperCase() ||
                  '',
                ...(initialContextVars || {}),
              },
              undefined, // Don't send initialQuestion after login, only show the login status chip
              birthDate,
              { loginToken: token } as any,
              undefined,
              sessionId
            ).then(state => {
              setShowLoginDrawer(false);
              setUser(user);
              setLoginToken(token);
              userToken = token;
              setLocalConfig('loginToken', token);
              // Push a message with initial status to show status message when a new session is created after login
              if (
                state?.sessionID &&
                state.sessionID !== sessionId &&
                state?.dialogState
              ) {
                // Push a message with initial status message showing successful login
                // Only show the chip component, not the emission text
                const username = user?.userName || t('login.user');
                pushMessage({
                  text: '', // Empty text so only the chip is visible
                  emitter: state.dialogState.emitter,
                  media:
                    state.dialogState.emittedMedia ??
                    state.dialogState.media ??
                    [],
                  fromUser: false,
                  initial: t('login.successfullyLoggedIn', { username }) as any,
                  contextVars: state.dialogState.contextVars,
                  date: state.dialogState.currentDate,
                  placeName: state.dialogState.currentPlaceName,
                  placeLatitude: state.dialogState.currentLatitude,
                  placeLongitude: state.dialogState.currentLongitude,
                  placeUncertaintyKm: state.dialogState.currentUncertaintyKm,
                  tag: state.dialogState.currentTag,
                  memoryTags: state.dialogState.memoryTags,
                });
                // Update the dialog state so the UI reflects the new session
                setCurrentDialogState(state.dialogState);
              }
            });
          }}
          setUser={setUser}
          onLogout={() => {
            if (!loginToken) return;
            client.backend.pwlUserLogout(loginToken).then(() => {
              setShowLoginDrawer(false);
              setUser(undefined);
              setLoginToken(undefined);
              userToken = undefined;
              removeLocalConfig('loginToken');
            });
          }}
        />
      )}
    </div>
  );
};

export default MemoriWidget;
