import React, {
  useEffect,
  useState,
  forwardRef,
  useImperativeHandle,
  useRef,
  useMemo,
} from 'react';
import { useEffectEvent } from '../hooks/useEffectEvent.js';
import {
  type Insights,
  useAccountId,
  useAppStateDispatch,
  useCompetitorUrls,
  useDraft,
  useDrupal,
  useExcludedCompetitorUrls,
  useKeywords,
  useRankSource,
} from '../Context.js';
import isEqual from 'react-fast-compare';
import useSWRImmutable from 'swr/immutable';
import { get, put, post } from './fetcher.js';

export interface DraftManagerRef {
  refreshInsights: () => Promise<void>;
  regenerateInsights: () => void;
}

type InsightsRaw =
  | {
      type: 'AUDIENCE_INSIGHT';
      status: 'COLLECTED' | 'ERROR';
      details: {
        audience: string;
        userDefinedTarget?: string;
      };
    }
  | {
      type: 'CONTENT_TYPE_INSIGHT';
      status: 'COLLECTED' | 'ERROR';
      details: {
        contentType: string;
        contentTypes: string[];
        userDefinedTarget?: string;
      };
    }
  | {
      type: 'JOURNEY_STAGE_INSIGHT';
      status: 'COLLECTED' | 'ERROR';
      details: {
        journeyStage: string;
        journeyStages: string[];
        userDefinedTarget?: string;
      };
    }
  | {
      type: 'OBJECTIVE_INSIGHT';
      status: 'COLLECTED' | 'ERROR';
      details: {
        objective: string;
        userDefinedTarget?: string;
      };
    };

const DraftManager = forwardRef<
  DraftManagerRef,
  {
    isLoading?: (_: boolean) => void;
    hasError?: (_: boolean) => void;
  }
>(({ isLoading = (_) => false, hasError = (_) => false }, ref) => {
  const dispatch = useAppStateDispatch();
  const draft = useDraft();
  const accountId = useAccountId();
  const keywords = useKeywords();
  const competitorUrls = useCompetitorUrls();
  const excludedCompetitorUrls = useExcludedCompetitorUrls();
  const rankSource = useRankSource();
  const drupal = useDrupal();

  const onError = useEffectEvent((error) => {
    hasError(error);
  });
  const onLoading = useEffectEvent((loading) => {
    isLoading(loading);
  });
  const onInsights = useEffectEvent((insights: Insights) => {
    dispatch({ type: 'setInsights', insights });
  });

  /**
   * Draft handling.
   */
  const draftData = useMemo(
    () => ({
      draftTitle: draft.title,
      phrases: keywords,
      competitorUrls: competitorUrls.filter((url: string) => !excludedCompetitorUrls.includes(url)),
      rankSourceId: rankSource.id,
      title: draft.title,
      metaDescription: draft.metaDescription,
      bodyCopy: draft.bodyCopy,
    }),
    [
      draft.title,
      draft.metaDescription,
      draft.bodyCopy,
      keywords,
      competitorUrls,
      excludedCompetitorUrls,
      rankSource.id,
    ],
  );
  const draftDataRef = useRef(draftData);
  useEffect(() => {
    draftDataRef.current = draftData;
  }, [draftData]);

  // Create a draft.
  const {
    data: createDraftData,
    error: createDraftError,
    isLoading: createDraftIsLoading,
  } = useSWRImmutable(
    !draft.id
      ? [`/conductor/proxy/v3/accounts/${accountId}/drafts/writing-assistant`, draftData]
      : null,
    post,
  );
  useEffect(() => {
    if (draft.id) {
      return;
    }

    if (!draft.id && createDraftData?.id) {
      dispatch({
        type: 'updateDraft',
        draft: { id: createDraftData.id, lastUpdated: createDraftData.updated },
      });
    }
    // Successful return, but no valid data.
    else if (createDraftData && !createDraftError && !createDraftIsLoading) {
      throw new Error('Unable to create draft');
    }
  }, [dispatch, draft.id, createDraftData, createDraftError, createDraftIsLoading]);

  // Link the draft with an entity in Drupal.
  const {
    data: drupalData,
    error: linkDraftError,
    isLoading: linkDraftIsLoading,
  } = useSWRImmutable(
    drupal.entity.id && drupal.entity.type && draft?.id && !drupal.draftLinked
      ? [
          `/conductor/api/draft/${drupal.entity.type}/${drupal.entity.id}`,
          {
            draftId: draft.id,
          },
        ]
      : null,
    post,
  );
  useEffect(() => {
    if (drupal.draftLinked) {
      return;
    }

    if (drupalData?.message === 'success') {
      dispatch({ type: 'setDrupalDraftLinked', draftLinked: true });
    }
    // Successful return, but no valid data.
    else if (drupalData && !linkDraftError && !linkDraftIsLoading) {
      if (drupalData?.error) {
        throw new Error(drupalData.error);
      }
      throw new Error('Unable link draft to Drupal');
    }
  }, [dispatch, drupal.draftLinked, drupalData, linkDraftError, linkDraftIsLoading]);

  // Update draft.
  //
  // @todo if this fails with the following, the last known update timestamp is out of date
  // {"timestamp":1763054169916,"status":400,"error":"Bad Request","path":"/customer-api/v3/accounts/24936/drafts/bead88f1-bd37-48a7-9861-46f04058141d/writing-assistant"}⏎
  // we should warn the user that someone else has made changes and make them reload the app, right now it will show the
  // generic error advising them to wait or reload.
  const consecutiveErrorsRef = useRef(0);

  const draftRef = useRef(draft);
  useEffect(() => {
    draftRef.current = draft;
  }, [draft]);

  const lastDraftUpdateRef = useRef({ ...draftData });

  // When updating a draft, a timestamp is returned which must be used in the subsequent draft update.
  // This means we need to make the update requests synchronously and ensure the previous one
  // finishes successfully before the next one starts. We poll for changes so we can do this in a
  // relatively simple way.
  useEffect(() => {
    let timeoutId: NodeJS.Timeout;
    let isMounted = true;

    const updateDraft = async () => {
      const currentDraftData = draftDataRef.current;
      const currentLastDraftUpdate = lastDraftUpdateRef.current;
      const currentDraft = draftRef.current;

      if (!isEqual(currentDraftData, currentLastDraftUpdate)) {
        try {
          const response = await put([
            `/conductor/proxy/v3/accounts/${accountId}/drafts/${currentDraft.id}/writing-assistant`,
            { ...currentDraftData, lastUpdateTime: currentDraft.lastUpdated },
          ]);

          if (!response?.updated) {
            throw new Error('Last update time not found');
          }

          if (!currentDraft.lastUpdated || response.updated > currentDraft.lastUpdated) {
            dispatch({ type: 'updateDraft', draft: { lastUpdated: response.updated } });
          }
          lastDraftUpdateRef.current = { ...currentDraftData };
          consecutiveErrorsRef.current = 0;
          onError(false);
        } catch {
          onError(true);
          consecutiveErrorsRef.current += 1;

          // If 30 consecutive requests fail, mark this as completed and don't try again until
          // the next data change.
          if (consecutiveErrorsRef.current >= 30) {
            lastDraftUpdateRef.current = { ...currentDraftData };
          }
        }
      }

      if (isMounted) {
        // Slow down requests if we've hit a failure.
        timeoutId = setTimeout(updateDraft, consecutiveErrorsRef.current > 0 ? 3000 : 1000);
      }
    };

    timeoutId = setTimeout(updateDraft, 1000);

    return () => {
      isMounted = false;
      if (timeoutId) clearTimeout(timeoutId);
    };
  }, [accountId, dispatch]);

  /**
   * Content Insights.
   *
   * This must be requested or content generation won't work.
   */

  // First see if content insights already exist.
  const [generateInsights, setGenerateInsights] = useState<boolean>(false);
  const [insightsRequestId, setInsightsRequestId] = useState<string | null>(null);

  const { data: existingInsightsData, error: existingInsightsError } = useSWRImmutable(
    draft.id
      ? `/conductor/proxy/v3/accounts/${accountId}/insights/${draft.id}/writing-assistant`
      : null,
    get,
  );
  useEffect(() => {
    if (existingInsightsError) {
      setGenerateInsights(true);
    }
  }, [existingInsightsError]);
  useEffect(() => {
    // Although we could get the data here, this is set to an arbitrary value so we can use the
    // fetching function below which handles waiting for all the data to finish processing if
    // necessary.
    if (existingInsightsData) {
      setInsightsRequestId('1');
    }
  }, [existingInsightsData]);

  // Request that insights be generated, this will return a request_id if successful.
  const {
    data: generateInsightsData,
    error: generateInsightsError,
    mutate: mutateGenerateInsights,
  } = useSWRImmutable(
    generateInsights
      ? [
          `/conductor/proxy/v3/accounts/${accountId}/insights/${draft.id}/writing-assistant`,
          {
            insight: {
              input: {
                topic: {
                  phrases: draftData.phrases,
                  rankSourceId: draftData.rankSourceId,
                  webPropertyId: 0,
                  competitorUrls: draftData.competitorUrls,
                },
              },
              request: {
                insights: [
                  'PAA_INSIGHT',
                  'BODY_CONTENT_COMMON',
                  'CONTENT_ANALYSIS_INSIGHT',
                  'HTML_HEADING1_COMMON',
                  'HTML_HEADING2_COMMON',
                  'HTML_HEADING3_COMMON',
                  'HTML_META_DESCRIPTION_COMMON',
                  'HTML_TITLE_COMMON',
                  'CONTENT_LENGTH_INSIGHT',
                  'READABILITY_INSIGHT',
                  'OBJECTIVE_INSIGHT',
                  'AUDIENCE_INSIGHT',
                  'JOURNEY_STAGE_INSIGHT',
                  'CONTENT_TYPE_INSIGHT',
                  'SUBTOPIC_INSIGHT',
                  'OPPORTUNITIES_SNIPPET',
                  'MSV_SNIPPET',
                ],
              },
            },
          },
        ]
      : null,
    post,
  );
  useEffect(() => {
    if (generateInsightsData?.request_id) {
      setInsightsRequestId(generateInsightsData.request_id);
    }
  }, [generateInsightsData]);

  // Fetch the insights.
  const {
    data: insightsData,
    error: insightsError,
    mutate: mutateInsights,
  } = useSWRImmutable(
    insightsRequestId
      ? `/conductor/proxy/v3/accounts/${accountId}/insights/${draft.id}/writing-assistant${
          insightsRequestId === '1'
            ? ''
            : `?${new URLSearchParams({
                requestUuid: insightsRequestId,
              })}`
        }`
      : null,
    get,
    {
      refreshInterval: (data) => {
        if (!data) return 250; // Keep polling if no data yet

        const isCompleted = (status: string) =>
          status === 'COMPLETED' || status === 'COMPLETED_WITH_ERROR';

        const allCompleted = isCompleted(data.status) && isCompleted(data.insightsStatus);

        return allCompleted ? 0 : 250; // Stop when completed, otherwise continue
      },
      errorRetryCount: 50,
      errorRetryInterval: 250,
    },
  );

  useEffect(() => {
    if (insightsData?.startTime && insightsData?.results?.insights?.length) {
      const newInsights = insightsData.results.insights
        .filter((insight: InsightsRaw) => insight.status === 'COLLECTED')
        .reduce(
          (acc: Insights, val: InsightsRaw) => {
            switch (val.type) {
              case 'AUDIENCE_INSIGHT':
                acc.data = {
                  ...acc.data,
                  audience: {
                    initialValue: val?.details?.audience ?? '',
                    value: val?.details?.userDefinedTarget ?? val?.details?.audience ?? '',
                    key: 'AUDIENCE_INSIGHT',
                  },
                };
                break;
              case 'CONTENT_TYPE_INSIGHT':
                acc.data = {
                  ...acc.data,
                  contentType: {
                    initialValue: val?.details?.contentType ?? '',
                    options: val?.details?.contentTypes ?? [],
                    value: val?.details?.userDefinedTarget ?? val?.details?.contentType ?? '',
                    key: 'CONTENT_TYPE_INSIGHT',
                  },
                };
                break;
              case 'OBJECTIVE_INSIGHT':
                acc.data = {
                  ...acc.data,
                  objective: {
                    initialValue: val?.details?.objective ?? '',
                    value: val?.details?.userDefinedTarget ?? val?.details?.objective ?? '',
                    key: 'OBJECTIVE_INSIGHT',
                  },
                };
                break;
              case 'JOURNEY_STAGE_INSIGHT':
                acc.data = {
                  ...acc.data,
                  journeyStage: {
                    initialValue: val?.details?.journeyStage,
                    options: val?.details?.journeyStages,
                    value: val?.details?.userDefinedTarget ?? val?.details?.journeyStage,
                    key: 'JOURNEY_STAGE_INSIGHT',
                  },
                };
                break;
            }
            return acc;
          },
          {
            generatedTime: insightsData.startTime,
            keywords: insightsData.request?.insight?.input?.topic?.phrases || [],
          },
        );

      onInsights(newInsights);
    }
  }, [insightsData]);

  useImperativeHandle(ref, () => ({
    refreshInsights: (): Promise<void> => {
      return mutateInsights();
    },
    regenerateInsights: () => {
      if (!generateInsights) {
        dispatch({ type: 'setInsights', insights: {} });
        setGenerateInsights(true);
      } else {
        dispatch({ type: 'setInsights', insights: {} });
        mutateGenerateInsights();
      }
    },
  }));

  /**
   * Loading and error handling.
   */
  useEffect(() => {
    const isLoading = !draft.id || !draft.lastUpdated || !drupal.draftLinked;
    onLoading(isLoading);
  }, [draft, drupal]);

  useEffect(() => {
    if (
      createDraftError ||
      linkDraftError ||
      generateInsightsError ||
      (insightsError && insightsError?.message !== 'Processing not complete')
    ) {
      onError(true);
    } else {
      onError(false);
    }
  }, [createDraftError, linkDraftError, generateInsightsError, insightsError]);

  return <></>;
});

DraftManager.displayName = 'DraftManager';

export default DraftManager;
