import { ReactNode, createContext, useCallback, useContext, useMemo } from 'react';
import { useAtomValue } from 'jotai';
import { baseUrlAtom, orgLevelApiKeyAtom } from 'src/atoms/envive/enviveConfig';
import { getMerchantOrgIdQuery } from 'src/application/models/graphql/queries/getMerchantOrgIdQuery';
import { validateGraphQLOrgId } from 'src/application/models/validators/validateGraphQLOrgId';
import { ColorMapping } from 'src/application/models/colorsConfig';
import { FrontendConfig } from 'src/application/models/frontendConfig';
import Logger from 'src/application/logging/logger';
import {
  CamelCasedPropertiesDeep,
  transformSnakeToCamel,
  validateAndTransformMountingConfig,
  validateAndTransformPageVariants,
} from 'src/application/models';
import { getColorsAndFrontendQuery } from 'src/application/models/graphql/queries/getColorsAndFrontendQuery';
import { FrontendConfigV3 } from 'src/application/models/frontendConfigV3';
import { ColorMappingV3 } from 'src/application/models/colorsConfigV3';
import { FloatingButtonLocation } from '@envive-ai/react-toolkit-v3/FloatingButton';
import { mockV3ColorsConfig, mockV3FrontendConfig } from './mockV3Config';
import { ConfigVersionEnum, WidgetConfigV3 } from '../typesV3';
import {
  OrgPageConfig,
  PageVariantConfig,
  PageVariantTestType,
  WidgetMountingConfig,
} from '../types';

export type ColorsConfigResponse = CamelCasedPropertiesDeep<ColorMapping>;
export type FrontendConfigResponse = CamelCasedPropertiesDeep<FrontendConfig>;
export type ColorsConfigV3Response = CamelCasedPropertiesDeep<ColorMappingV3>;
export type FrontendConfigV3Response = CamelCasedPropertiesDeep<FrontendConfigV3>;

export type GraphQlConfigValues = {
  colorsConfig?: ColorsConfigResponse | ColorsConfigV3Response;
  frontendConfig?: CamelCasedPropertiesDeep<FrontendConfig | FrontendConfigV3>;
  orgPageConfig?: OrgPageConfig;
};

interface GraphQLContextValue {
  executeQuery: <T>(query: string, variables?: Record<string, unknown>) => Promise<T>;
  getOrgId: () => Promise<string | undefined>;
  getColorsAndFrontendConfig: () => Promise<GraphQlConfigValues>;
  getV3Config: () => Promise<GraphQlConfigValues>;
  getConfig: () => Promise<GraphQlConfigValues>;
  isReady: boolean;
}

const GraphQLContext = createContext<GraphQLContextValue | null>(null);

const logger = new Logger('graphqlContext');

type GraphQLProviderProps = {
  children: ReactNode;
  requestV3Config?: boolean;
  mockV3ConfigToDeprecatedConfig?: boolean;
};

const DEFAULT_PAGE_VARIANTS: PageVariantConfig[] = [
  {
    variantId: 'plp',
    variantType: 'plp',
    plpIdExtractor: 'url-resolver-plp-id',
    widgetMounting: [],
    variantTests: [
      {
        testType: PageVariantTestType.UrlResolver,
      },
    ],
  },
  {
    variantId: 'pdp',
    variantType: 'pdp',
    productIdExtractor: 'url-resolver-product-id',
    widgetMounting: [],
    variantTests: [
      {
        testType: PageVariantTestType.UrlResolver,
      },
    ],
  },
];

const mapPositionToCamelCase = (position: string): FloatingButtonLocation => {
  if (position === 'bottom-left') {
    return FloatingButtonLocation.BOTTOM_LEFT;
  }
  if (position === 'middle-left') {
    return FloatingButtonLocation.MIDDLE_LEFT;
  }
  if (position === 'middle-right') {
    return FloatingButtonLocation.MIDDLE_RIGHT;
  }
  if (position === 'bottom-right') {
    return FloatingButtonLocation.BOTTOM_RIGHT;
  }
  throw new Error(`Invalid position: ${position}`);
};

/**
 * This function is used to do some additional cleanup of the v3 frontend config to ensure that the config is in the correct format.
 */
const v3FrontendConfigCleanup = (
  v3RootConfig: FrontendConfigV3Response,
): FrontendConfigV3Response => {
  const { uiConfigs } = v3RootConfig;
  if (!uiConfigs) {
    return v3RootConfig;
  }
  const { floatingButton, lookAndFeel } = uiConfigs as any;
  if (!floatingButton || !lookAndFeel) {
    return v3RootConfig;
  }
  const { fontSize, lineHeight } = lookAndFeel.typography as any;
  return {
    ...v3RootConfig,
    uiConfigs: {
      ...uiConfigs,
      floatingButton: {
        ...floatingButton,
        position: mapPositionToCamelCase(
          v3RootConfig.uiConfigs?.floatingButton?.position as string,
        ),
      },
      lookAndFeel: {
        ...lookAndFeel,
        chatHeaderLogoDarkSrc: lookAndFeel.chatHeaderLogoDarkSrc || lookAndFeel.chatHeaderLogoSrc,
        chatHeaderLogoLightSrc: lookAndFeel.chatHeaderLogoLightSrc || lookAndFeel.chatHeaderLogoSrc,
        typography: {
          ...lookAndFeel.typography,
          fontSize: {
            // The fonts sizes are defined differently in the backend config
            // so we need to map the values to the correct camel case keys
            b1: fontSize?.b1 || fontSize?.b_1,
            b2: fontSize?.b2 || fontSize?.b_2,
            b3: fontSize?.b3 || fontSize?.b_3,
            b4: fontSize?.b4 || fontSize?.b_4,
            b5: fontSize?.b5 || fontSize?.b_5,
            h1: fontSize?.h1 || fontSize?.h_1,
            h2: fontSize?.h2 || fontSize?.h_2,
            h3: fontSize?.h3 || fontSize?.h_3,
            l1: fontSize?.l1 || fontSize?.l_1,
            l2: fontSize?.l2 || fontSize?.l_2,
            t1: fontSize?.t1 || fontSize?.t_1,
            t2: fontSize?.t2 || fontSize?.t_2,
            t3: fontSize?.t3 || fontSize?.t_3,
          },
          lineHeight: {
            ...lookAndFeel.typography.lineHeight,
            '114': lineHeight?.lh_114 || lineHeight?.[114],
            '116': lineHeight?.lh_116 || lineHeight?.[116],
            '118': lineHeight?.lh_118 || lineHeight?.[118],
            '120': lineHeight?.lh_120 || lineHeight?.[120],
            '124': lineHeight?.lh_124 || lineHeight?.[124],
            '128': lineHeight?.lh_128 || lineHeight?.[128],
            '130': lineHeight?.lh_130 || lineHeight?.[130],
            '133': lineHeight?.lh_133 || lineHeight?.[133],
            '140': lineHeight?.lh_140 || lineHeight?.[140],
            '148': lineHeight?.lh_148 || lineHeight?.[148],
          },
        },
      },
    },
  };
};

export const GraphQLProvider = ({
  children,
  requestV3Config = false,
  mockV3ConfigToDeprecatedConfig = false,
}: GraphQLProviderProps) => {
  const apiKey = useAtomValue(orgLevelApiKeyAtom);
  const baseUrl = useAtomValue(baseUrlAtom);
  const isReady = useMemo(() => Boolean(apiKey && baseUrl), [apiKey, baseUrl]);

  const executeQuery = useCallback(
    async (query: string, variables?: Record<string, unknown>) => {
      if (!isReady) {
        throw new Error('GraphQL client not ready - missing apiKey or baseUrl');
      }

      const response = await fetch(`${baseUrl}/v1/graphql`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          Authorization: `Bearer ${apiKey}`,
        },
        body: JSON.stringify({ query, variables }),
      });

      if (!response.ok) {
        throw new Error(`GraphQL request failed: ${response.statusText}`);
      }

      const result = await response.json();
      if (result.errors) {
        throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
      }

      return result.data;
    },
    [apiKey, baseUrl, isReady],
  );

  const getOrgId = useCallback(async () => {
    const response = await executeQuery(getMerchantOrgIdQuery);
    return validateGraphQLOrgId(response.me.org?.id);
  }, [executeQuery]);

  const getV3Config = useCallback(async (): Promise<GraphQlConfigValues> => {
    try {
      const query = await getColorsAndFrontendQuery();
      if (!query) {
        throw new Error('Colors and frontend config query is not defined');
      }
      const response = await executeQuery(query);

      // We may update this root config location in the future
      const v3RootConfig = response.me.getProductsConfigByVersion?.v_three_config?.values;
      const frontendValues = response.me.getProductsConfigByVersion?.frontend?.values;

      const v3FrontendConfig = v3FrontendConfigCleanup(
        transformSnakeToCamel(v3RootConfig) as FrontendConfigV3Response,
      );

      // Get merchant_override_css from frontend.values if v3RootConfig is not available
      const frontendMerchantOverrideCss = frontendValues?.merchant_override_css;
      const v3MerchantOverrideCss = v3FrontendConfig.uiConfigs?.merchantOverrideCss;
      const merchantOverrideCss = v3MerchantOverrideCss || frontendMerchantOverrideCss;

      const v3MountingConfigsArray = (transformSnakeToCamel(v3RootConfig?.mounting_configs) ??
        []) as unknown as { key: string; config: WidgetMountingConfig }[];

      const v3MountingConfigs = v3MountingConfigsArray.reduce(
        (acc, { key, config }) => {
          acc[key] = validateAndTransformMountingConfig(config, key);
          return acc;
        },
        {} as Record<string, WidgetMountingConfig>,
      );

      const v3widgetConfigsArray = (transformSnakeToCamel(v3RootConfig?.widget_configs) ??
        []) as unknown as WidgetConfigV3[];
      const v3WidgetConfigs = Object.fromEntries(
        v3widgetConfigsArray?.map(({ widgetConfigId, ...rest }: WidgetConfigV3) => [
          widgetConfigId,
          { widgetConfigId, ...rest } as WidgetConfigV3,
        ]),
      ) as Record<string, WidgetConfigV3>;
      const v3pageVariants: PageVariantConfig[] = Array.isArray(v3RootConfig?.page_variants)
        ? v3RootConfig?.page_variants.map(validateAndTransformPageVariants)
        : DEFAULT_PAGE_VARIANTS;

      const v3ColorsConfig = transformSnakeToCamel(v3RootConfig?.colors?.values);
      const isSemanticColors = !window.location.href.includes('globals=merchant');
      const isStorybook = window.top?.location.href.includes('?path=');

      // If no v3 colors are found, return the default mock config to allow unconfigured merchants to work by default
      if (!v3RootConfig?.colors || (isStorybook && isSemanticColors)) {
        logger.logDebug('GraphQLContext | Returning mock v3 config', {
          colorsConfig: mockV3ColorsConfig,
          frontendConfig: mockV3FrontendConfig as FrontendConfigV3Response,
        });
        const colorsConfig = mockV3ColorsConfig;
        const frontendConfig = mockV3FrontendConfig;
        return {
          colorsConfig,
          frontendConfig: frontendConfig as FrontendConfigV3Response,
          orgPageConfig: {
            pageVariants: DEFAULT_PAGE_VARIANTS,
            widgetConfigs: {},
            mountingConfigs: {},
          },
        };
      }

      logger.logDebug('GraphQLContext | Returning v3 config', {
        colorsConfig: v3ColorsConfig as ColorsConfigV3Response,
        frontendConfig: v3FrontendConfig,
        orgPageConfig: {
          pageVariants: v3pageVariants,
          widgetConfigs: v3WidgetConfigs,
          mountingConfigs: v3MountingConfigs,
        },
      });

      return {
        colorsConfig: v3ColorsConfig as ColorsConfigV3Response,
        frontendConfig: {
          ...(v3FrontendConfig as FrontendConfigV3Response),
          merchantOverrideCss,
          // Need to replace the automatic widget configs with the object based ones
          widgetConfigs: v3WidgetConfigs,
        },
        orgPageConfig: {
          pageVariants: v3pageVariants,
          widgetConfigs: v3WidgetConfigs,
          mountingConfigs: v3MountingConfigs,
        },
        // TODO: Add widgets and other config here
      };
    } catch (err) {
      logger.logError('Error fetching graphql colors and frontend config', err);
      return { colorsConfig: undefined, frontendConfig: undefined };
    }
  }, [executeQuery, mockV3ConfigToDeprecatedConfig]);

  const getColorsAndFrontendConfig = useCallback(async (): Promise<GraphQlConfigValues> => {
    try {
      const query = await getColorsAndFrontendQuery();
      if (!query) {
        throw new Error('Colors and frontend config query is not defined');
      }
      const response = await executeQuery(query);
      if (
        response.me.getProductsConfigByVersion?.version !== ConfigVersionEnum.V3 &&
        !mockV3ConfigToDeprecatedConfig
      ) {
        const colorsConfig = response.me.getProductsConfigByVersion?.colors?.values;
        const frontendConfig = response.me.getProductsConfigByVersion?.frontend?.values;
        const transformedColorConfig = transformSnakeToCamel(colorsConfig);
        const transformedFrontendConfig = transformSnakeToCamel(frontendConfig);
        return {
          colorsConfig: transformedColorConfig as ColorsConfigResponse,
          frontendConfig: transformedFrontendConfig as FrontendConfigResponse,
        };
      }
      // TODO: REMOVE MOCKED CONFIG ONCE WE HAVE THE NEW CONFIG IMPLEMENTED ON BACKEND
      const merchantColorsConfig = response.me.getProductsConfigByVersion?.colors?.values;
      const transformedMerchantColorConfig = transformSnakeToCamel(merchantColorsConfig);

      const isSemanticColors = !window.location.href.includes('globals=merchant');

      const colorsConfig = mockV3ColorsConfig;
      const frontendConfig = mockV3FrontendConfig;

      return {
        colorsConfig: isSemanticColors
          ? (colorsConfig as ColorsConfigV3Response)
          : (transformedMerchantColorConfig as ColorsConfigV3Response),
        frontendConfig: frontendConfig as FrontendConfigV3Response,
      };
    } catch (err) {
      logger.logError('Error fetching graphql colors and frontend config', err);
      return { colorsConfig: undefined, frontendConfig: undefined };
    }
  }, [executeQuery]);

  const getConfig = useCallback(async (): Promise<GraphQlConfigValues> => {
    if (requestV3Config) {
      logger.logDebug('GraphQLContext | Getting v3 config', requestV3Config);
      return getV3Config();
    }
    logger.logDebug('GraphQLContext | Getting colors and frontend config', requestV3Config);
    return getColorsAndFrontendConfig();
  }, [getColorsAndFrontendConfig, getV3Config, requestV3Config]);

  const value = useMemo(
    () => ({
      executeQuery,
      getOrgId,
      getColorsAndFrontendConfig,
      getV3Config,
      getConfig,
      isReady,
    }),
    [executeQuery, getOrgId, getColorsAndFrontendConfig, getV3Config, getConfig, isReady],
  );

  return <GraphQLContext.Provider value={value}>{children}</GraphQLContext.Provider>;
};

export const useGraphQLClient = () => {
  const context = useContext(GraphQLContext);
  if (!context) {
    throw new Error('useGraphQLClient must be used within a GraphQLProvider');
  }
  return context;
};
