import { UserEventCategory } from '@spiffy-ai/commerce-api-client';
import { useAtomValue, useSetAtom } from 'jotai';
import { useCallback } from 'react';
import CommerceApiClient from 'src/application/commerce-api';
import Logger from 'src/application/logging/logger';
import {
  GenerationParams,
  Message,
  MessageRole,
  MessageType,
  NextMessageRequest,
  Response,
  UserEvent,
} from 'src/application/models';
import { messageFromResponse } from 'src/application/utils';
import { appDetailsAtom } from 'src/atoms/app';
import { analyticsContextAtom } from 'src/atoms/app/variant';
import {
  messagesAtom,
  pendingResponseAtom,
  requestFailureAtom,
  responseStreamingAtom,
  suggestionsAtom,
} from 'src/atoms/chat/chatState';
import { useAmplitude } from 'src/contexts/amplitudeContext';
import { useFeatureFlagService } from 'src/contexts/featureFlagServiceContext';
import { useMessageInterceptor } from 'src/interceptors/useMessageInterceptor';
import {
  EnviveMetricsEventName,
  SpiffyMetricsEventName,
} from 'src/services/amplitudeService/amplitudeService';
import { v4 as uuid } from 'uuid';

import { StatusCodeError } from './statusCodeError';

const logger = new Logger('salesAgentService');

interface SalesAgentService {
  createResponsePayload: (payload: {
    userEvents: UserEvent[];
    generationParams?: GenerationParams;
  }) => NextMessageRequest;
  getStreamingResponses: (payload: NextMessageRequest) => Promise<void>;
  getSuggestions: (payload: NextMessageRequest) => Promise<void>;
  hydrateMessages: () => Promise<void>;
}

const inputPropsToTrackingProps = (
  payload: NextMessageRequest,
): Record<string, unknown> | undefined => {
  const [userEvent] = payload.userEvents || [];
  if (userEvent.category === UserEventCategory.SuggestionClicked) {
    return {
      message_id: userEvent.eventId,
      user_event_type: 'suggestion_clicked',
      user_query: userEvent.attributes.content,
    };
  }
  if (userEvent.category === UserEventCategory.QueryTyped) {
    return {
      message_id: userEvent.eventId,
      user_event_type: 'query_typed',
      user_query: userEvent.attributes.query,
    };
  }
  return {};
};

const inputPropsToEnviveTrackingProps = (
  payload: NextMessageRequest,
): Record<string, unknown> | undefined => {
  const [userEvent] = payload.userEvents || [];
  if (userEvent.category === UserEventCategory.SuggestionClicked) {
    return {
      'chat.request_type': 'suggestion',
      'chat.request_id': userEvent.eventId,
      'chat.request_text': userEvent.attributes.content,
      'chat.suggestion_id': userEvent.attributes.suggestionId,
      'chat.user_typed': false,
    };
  }
  if (userEvent.category === UserEventCategory.QueryTyped) {
    return {
      'chat.request_type': 'text',
      'chat.request_id': userEvent.eventId,
      'chat.request_text': userEvent.attributes.query,
      'chat.user_typed': userEvent.attributes.userTyped,
    };
  }
  if (
    userEvent.category === UserEventCategory.PageVisit ||
    userEvent.category === UserEventCategory.PdpVisit ||
    userEvent.category === UserEventCategory.PlpVisit
  ) {
    return {
      'chat.request_id': userEvent.eventId,
      'chat.request_type': 'page_visit',
    };
  }

  return {};
};

export const getQueryParam = (key: string): string | null => {
  const urlObj = new URL(window.location.href);
  return urlObj.searchParams.get(key);
};

const getErrorMessage = (_error: unknown): string => {
  if (_error instanceof StatusCodeError) {
    if (_error.statusCode === 423) {
      return 'Due to violation of terms and services, this session has been terminated.';
    }
    // Keeping the 429 error messages as the generic error message for now.
    // if (_error.statusCode === 429) {
    //   return "I'm sorry! The system is busy right now. Please try again in a few minutes.";
    // }
  }

  return "I'm sorry! I'm having trouble right now. Please refresh the page or try again in a moment.";
};

const handleStreamingError = (
  _error: unknown,
  setRequestFailure: (failed: boolean) => void,
  setMessages: (updater: (prev: Message[][]) => Message[][]) => void,
) => {
  setRequestFailure(true);

  const errorMessage = getErrorMessage(_error);
  setMessages(prev => [
    ...prev,
    [
      {
        id: uuid(),
        role: MessageRole.Assistant,
        type: MessageType.Text,
        createdAt: new Date().toISOString(),
        metadata: {
          content: errorMessage,
        },
      },
    ],
  ]);
};

const updateMessageState = (
  message: Message,
  lastMessage: Message,
  setMessages: (updater: (prev: Message[][]) => Message[][]) => void,
): Message => {
  if (lastMessage == null) {
    setMessages(prev => [...prev, [message]]);
    return message;
  }
  if (lastMessage.type === MessageType.Text && message.type === MessageType.Text) {
    const newMessage = {
      ...lastMessage,
      metadata: {
        ...lastMessage.metadata,
        content: lastMessage.metadata.content + message.metadata.content,
      },
    };
    setMessages(prev => {
      const lastTurn = prev[prev.length - 1];
      return [
        ...prev.slice(0, prev.length - 1),
        [...lastTurn.slice(0, lastTurn.length - 1), newMessage],
      ];
    });
    return newMessage;
  }
  setMessages(prev => [...prev.slice(0, prev.length - 1), [...prev[prev.length - 1], message]]);
  return message;
};

const processStreamingResponse = async (
  stream: AsyncIterable<Response>,
  messageInterceptor: { intercept: (response?: Response) => boolean | undefined },
  setMessages: (updater: (prev: Message[][]) => Message[][]) => void,
  setResponseStreaming: (responseStreaming: boolean) => void,
): Promise<Record<string, number | string[]>> => {
  let lastMessage: Message | undefined;
  setResponseStreaming(true);
  const responseAnalytics: Record<string, number | string[]> = {};
  const productIds = [];

  for await (const response of stream) {
    try {
      if (messageInterceptor.intercept(response)) {
        return {};
      }

      if (!response) {
        throw new Error('No response from stream');
      }

      const message = messageFromResponse(response);
      if (!message) {
        throw new Error('Failed to transform API response to client message');
      }
      if (message.type === MessageType.Product) {
        const cardsCounter = (responseAnalytics['chat.product_cards_returned'] as number) || 0;
        responseAnalytics['chat.product_cards_returned'] = cardsCounter + 1;

        const productIds = responseAnalytics['chat.product_ids_returned'] as string[];
        if (!productIds) {
          responseAnalytics['chat.product_ids_returned'] = [message.metadata.id];
        } else {
          productIds.push(message.metadata.id);
        }
      }
      if (message.type === MessageType.Review) {
        const cardsCounter = (responseAnalytics['chat.review_cards_returned'] as number) || 0;
        responseAnalytics['chat.review_cards_returned'] = cardsCounter + 1;
      }

      // No support for ChatSearch messages at the current time.
      // Perhaps we add support back in the future
      // if (message.type === MessageType.ProductSearch) {
      //   handleSearchResults(message);
      //   hasSearchResults = true;
      //   setSearchIsLoading(false); // Update search loading immediately when results are detected
      // }

      lastMessage = updateMessageState(message, lastMessage!, setMessages);
    } catch (error: unknown) {
      logger.logWarn('Failed to generate responses from stream', error, {
        lastResponse: lastMessage,
        response,
      });
    }
    setResponseStreaming(false);
  }
  return responseAnalytics;
};

export const useSalesAgentService: () => SalesAgentService = () => {
  const setRequestFailure = useSetAtom(requestFailureAtom);
  const setMessages = useSetAtom(messagesAtom);
  const setSuggestions = useSetAtom(suggestionsAtom);
  const setPendingResponse = useSetAtom(pendingResponseAtom);
  const setResponseStreaming = useSetAtom(responseStreamingAtom);
  const analyticsContext = useAtomValue(analyticsContextAtom);
  const messageInterceptor = useMessageInterceptor();
  const { trackEvent } = useAmplitude();

  const featureFlagService = useFeatureFlagService();
  const context = useAtomValue(appDetailsAtom);

  const createResponsePayload = useCallback(
    ({
      userEvents,
      generationParams,
    }: {
      userEvents: UserEvent[];
      generationParams?: GenerationParams;
    }): NextMessageRequest => {
      const featureFlags = featureFlagService?.featureFlagService?.getFeatureFlags() || {};

      const overrideConfigVersion =
        getQueryParam('spiffy_config_version') ||
        getQueryParam('envive_config_version') ||
        undefined;
      const overrideModelDatetime = getQueryParam('override_model_datetime') || undefined;
      return {
        id: uuid(),
        context,
        userEvents,
        featureFlags,
        generationParams,
        overrideConfigVersion,
        overrideModelDatetime,
      };
    },
    [context, featureFlagService?.featureFlagService],
  );

  const getStreamingResponses = useCallback(
    async (payload: NextMessageRequest): Promise<void> => {
      const startTime = Date.now();
      let successfulResponse: boolean | undefined;
      setPendingResponse(true);
      setSuggestions([]);
      const stream = CommerceApiClient.getNextResponseStreaming(payload);
      let responseAnalytics: Record<string, unknown> | undefined;
      try {
        setRequestFailure(false);

        responseAnalytics = await processStreamingResponse(
          stream,
          messageInterceptor,
          setMessages,
          setResponseStreaming,
        );
        successfulResponse = true;

        // TODO: Add support for the Chrome Extension communication
        // await logBundleEvent({
        //   level: 'info',
        //   event: 'NEXT_RESPONSE_SUCCESS',
        //   context: {
        //     type: 'next_responses',
        //     endpoint: '/v1/next_responses',
        //     statusCode: 200,
        //     responseTime,
        //   },
        // });
      } catch (e) {
        console.error('error getting streaming responses', e);
        successfulResponse = false;
        // Log failed next_responses call
        // const responseTime = Date.now() - startTime;
        // await logBundleEvent({
        //   level: 'error',
        //   event: 'NEXT_RESPONSE_FAILED',
        //   message: `Failed to get streaming response: ${e}`,
        //   context: {
        //     type: 'next_responses',
        //     endpoint: '/v1/next_responses',
        //     responseTime,
        //     error: e instanceof Error ? e.message : String(e),
        //   },
        // });

        handleStreamingError(e, setRequestFailure, setMessages);
        throw e;
      } finally {
        // Log next_responses call
        const responseTime = Date.now() - startTime;
        trackEvent({
          eventName: SpiffyMetricsEventName.ChatAssistantResponse,
          eventProps: {
            responseTimeMs: responseTime.toString(),
            successful_response: successfulResponse,
            ...inputPropsToTrackingProps(payload),
          },
        });
        trackEvent({
          eventName: EnviveMetricsEventName.ChatResponse,
          eventProps: {
            ...analyticsContext,
            'chat.response_time_ms': responseTime.toString(),
            ...inputPropsToEnviveTrackingProps(payload),
            ...responseAnalytics,
          },
        });
        setPendingResponse(false);
        setResponseStreaming(false);
        // logPerfMetric(PerfMetricsEvents.FirstResponseCompleted);
      }
    },
    [
      analyticsContext,
      trackEvent,
      setResponseStreaming,
      // logPerfMetric,
      setRequestFailure,
      setSuggestions,
      messageInterceptor,
      setMessages,
      setPendingResponse,
    ],
  );

  const getSuggestions = useCallback(
    async (payload: NextMessageRequest): Promise<void> => {
      const newSuggestions = await CommerceApiClient.getNextSuggestions(payload);
      setSuggestions(newSuggestions);
    },
    [setSuggestions],
  );

  const hydrateMessages = useCallback(async () => {
    const { orgId, chatId, userId } = context;
    const { messages: existingMessages } = await CommerceApiClient.getResponses(
      orgId,
      chatId,
      userId,
    );
    setMessages([...existingMessages]);
  }, [context, setMessages]);

  return {
    createResponsePayload,
    getStreamingResponses,
    getSuggestions,
    hydrateMessages,
  };
};
