import {
  defaultCacheSize,
  prodLogsEndpointUrl,
  prodEdgeCdnBaseUrl,
  uniqueId,
  sdkVersion,
} from "../shared";
import {
  LogsHandler,
  CreateOptions,
  LocalLogger,
  LogLevel,
  ObjectValue,
  ObjectValueWithVariables,
  Query,
} from "../shared/types";
import Context from "./Context";
import Node, { NodeProps } from "./Node";
import Logger from "./Logger";
import { isBrowser } from "./environment";
import getMetadata from "../shared/helpers/getMetadata";
import HypertuneEdgeInitDataProvider from "./initDataProviders/HypertuneEdgeInitDataProvider";
import VercelEdgeConfigInitDataProvider from "./initDataProviders/VercelEdgeConfigInitDataProvider";
import defaultLocalLogger from "../shared/helpers/defaultLocalLogger";

const minIntervalMs = 1_000;
const defaultIntervalMs = 2_000;

/**
 * TODO
 * - Remove `queryCode`; consolidate with `query`; benefit of having them
 * separate was `queryCode` can be codegen'd differently, but this is not
 * necessary
 * - Add default values to arguments where possible
 * - Move all arguments into `CreateOptions` with type parameters if they have
 * generated types, then codegen concrete `CreateSourceOptions`
 */

export default function create<T extends Node = Node>({
  NodeConstructor = Node as new (props: NodeProps) => T,
  token,
  query = null,
  queryId,
  queryCode,
  variableValues = {},
  override = null,
  options = {},
}: {
  NodeConstructor?: new (props: NodeProps) => T;
  token: string;
  query?: Query<ObjectValueWithVariables> | null;
  queryId?: string;
  queryCode?: string;
  variableValues?: ObjectValue;
  override?: object | null;
  options?: CreateOptions;
}): T {
  const traceId = uniqueId();
  const loggerId = uniqueId();
  const logsHandler = wrapLogsHandler(
    loggerId,
    options.logsHandler,
    options.localLogger
  );
  const localLogger = wrapLocalLogger(
    loggerId,
    options.logsHandler,
    options.localLogger
  );

  let logger: Logger;
  const remoteLoggingMode =
    options.remoteLogging?.mode ?? (isBrowser ? "session" : "normal");

  try {
    logger = new Logger({
      id: loggerId,
      traceId,
      token,
      remoteLoggingMode,
      remoteFlushIntervalMs:
        options.remoteLogging?.flushIntervalMs !== undefined
          ? options.remoteLogging.flushIntervalMs === null ||
            options.remoteLogging.flushIntervalMs === 0 // TODO: Deprecate
            ? null
            : clampInterval(options.remoteLogging.flushIntervalMs)
          : defaultIntervalMs,
      remoteLoggingEndpointUrl:
        options.remoteLogging?.endpointUrl ?? prodLogsEndpointUrl,
      logsHandler,
      localLogger,
    });
  } catch (error) {
    localLogger(LogLevel.Error, "Pre-initialize error.", getMetadata(error));

    return new NodeConstructor({
      context: null,
      logger: null,
      parent: null,
      step: null,
      expression: null,
      initDataHash: null,
    });
  }

  const usesVercelEdgeConfig =
    options.initDataProvider &&
    options.initDataProvider instanceof VercelEdgeConfigInitDataProvider;

  logger.log(LogLevel.Debug, null, "Detected environment.", {
    isBrowser,
    usesVercelEdgeConfig,
  });

  try {
    const context = new Context({
      traceId,
      query,
      initQuery: queryId
        ? { type: "StoredQuery", id: queryId }
        : queryCode
          ? { type: "GraphqlQuery", code: queryCode }
          : { type: "Query", query },
      variableValues,
      initData: options.initData ?? null,
      lastInitDataRefreshTime:
        options.lastInitDataRefreshTime ??
        options.lastDataProviderInitTime ??
        null,
      initDataProvider:
        options.initDataProvider !== undefined
          ? options.initDataProvider
          : new HypertuneEdgeInitDataProvider({
              token,
              baseUrl: prodEdgeCdnBaseUrl,
              branchName: options.branchName,
              localLogger,
            }),
      initDataRefreshIntervalMs:
        // Prioritize the new option else use the old option if it's not set to
        // 0 else use the default interval. In all cases, force the value to be
        // at least the minimum interval.
        clampInterval(
          options.initDataRefreshIntervalMs ??
            (options.initIntervalMs || defaultIntervalMs)
        ),
      shouldRefreshInitData:
        // Prioritize the new option else if the old option is set to 0,
        // disable, else enable except when using Vercel Edge Config.
        options.shouldRefreshInitData ??
        options.shouldCheckForUpdates ??
        (options.initIntervalMs === 0 ? false : !usesVercelEdgeConfig),
      shouldRefreshInitDataOnCreate:
        options.shouldRefreshInitDataOnCreate ?? !usesVercelEdgeConfig,
      shouldSkipInitDataUpdateOnRefresh:
        options.shouldSkipInitDataUpdateOnRefresh ?? false,
      logger,
      cacheSize: options.cacheSize ?? defaultCacheSize,
      override: override ?? null,
    });

    return new NodeConstructor({
      context,
      logger,
      parent: null,
      step: null,
      expression: context.initData?.reducedExpression ?? null,
      initDataHash: context.initData?.hash ?? null,
    });
  } catch (error) {
    logger.log(
      LogLevel.Error,
      /* commitId */ null,
      "Initialize error.",
      getMetadata(error)
    );
    return new NodeConstructor({
      context: null,
      logger,
      parent: null,
      step: null,
      expression: null,
      initDataHash: null,
    });
  }
}

function clampInterval(intervalMs: number): number {
  return Math.max(intervalMs, minIntervalMs);
}

function wrapLogsHandler(
  loggerId: string,
  logsHandler: LogsHandler | undefined,
  localLogger: LocalLogger | undefined
): LogsHandler {
  if (!localLogger && !logsHandler) {
    return (reductionLogs) => {
      reductionLogs.messageList.forEach(({ level, message, metadata }) =>
        defaultLocalLogger(level, message, metadata)
      );
    };
  }

  return (reductionLogs) => {
    const messages =
      !logsHandler && !localLogger
        ? reductionLogs.messageList
        : reductionLogs.messageList.map(({ level, message, metadata }) => ({
            level,
            message,
            metadata: {
              sdkVersion,
              loggerId,
              ...metadata,
            },
          }));

    if (logsHandler) {
      logsHandler({ ...reductionLogs, messageList: messages });
      return;
    }
    if (localLogger) {
      messages.forEach(({ level, message, metadata }) => {
        localLogger(level, message, metadata);
      });
    }
  };
}

function wrapLocalLogger(
  loggerId: string,
  logsHandler: LogsHandler | undefined,
  localLogger: LocalLogger | undefined
): LocalLogger {
  if (!localLogger && !logsHandler) {
    return defaultLocalLogger;
  }
  return (level, message, baseMetadata) => {
    const metadata = {
      sdkVersion,
      loggerId,
      ...baseMetadata,
    };

    if (logsHandler) {
      logsHandler({
        messageList: [{ level, message, metadata }],
        eventList: [],
        exposureList: [],
        evaluationList: [],
      });
      return;
    }
    if (localLogger) {
      localLogger(level, message, metadata);
    }
  };
}
