import { evaluateFeature, evaluateFeatures, evaluateFeaturesByFlagSets } from '../evaluator';
import { thenable } from '../utils/promise/thenable';
import { getMatching, getBucketing } from '../utils/key';
import { validateDefinitionExistence } from '../utils/inputValidation/definitionExistence';
import { SDK_NOT_READY } from '../utils/labels';
import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, TREATMENTS_BY_FLAGSETS, TREATMENTS_BY_FLAGSET, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG, GET_TREATMENTS_BY_FLAG_SETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, GET_TREATMENTS_BY_FLAG_SET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, GET_TREATMENT_WITH_CONFIG, GET_TREATMENT, GET_TREATMENTS } from '../utils/constants';
import { IEvaluationResult } from '../evaluator/types';
import SplitIO from '../../types/splitio';
import { IMPRESSION_QUEUEING } from '../logger/constants';
import { ISdkFactoryContext } from '../sdkFactory/types';
import { isConsumerMode } from '../utils/settingsValidation/mode';
import { Method } from '../sync/submitters/types';
import { ImpressionDecorated } from '../trackers/types';
import { trackMethodFactory } from './trackMethod';

const treatmentNotReady = { treatment: CONTROL, label: SDK_NOT_READY };

function treatmentsNotReady(featureFlagNames: string[]) {
  const evaluations: Record<string, IEvaluationResult> = {};
  featureFlagNames.forEach(featureFlagName => {
    evaluations[featureFlagName] = treatmentNotReady;
  });
  return evaluations;
}

export function stringify(options?: SplitIO.EvaluationOptions) {
  if (options && options.properties) {
    try {
      return JSON.stringify(options.properties);
    } catch { /* JSON.stringify should never throw with validated options, but handling just in case */ }
  }
}

/**
 * Creator of base client with getTreatments and track methods.
 */
export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | SplitIO.IAsyncClient {
  const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, telemetryTracker, fallbackCalculator } = params;
  const { log, mode } = settings;
  const isAsync = isConsumerMode(mode);

  function getTreatment(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENT) {
    const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENT_WITH_CONFIG : TREATMENT);

    const wrapUp = (evaluationResult: IEvaluationResult) => {
      const queue: ImpressionDecorated[] = [];
      const treatment = processEvaluation(evaluationResult, featureFlagName, key, stringify(options), withConfig, methodName, queue);
      impressionsTracker.track(queue, attributes);

      stopTelemetryTracker(queue[0] && queue[0].imp.label);
      return treatment;
    };

    const evaluation = readinessManager.isReadyFromCache() ?
      evaluateFeature(log, key, featureFlagName, attributes, storage, options) :
      isAsync ? // If the SDK is not ready, treatment may be incorrect due to having splits but not segments data, or storage is not connected
        Promise.resolve(treatmentNotReady) :
        treatmentNotReady;

    return thenable(evaluation) ? evaluation.then((res) => wrapUp(res)) : wrapUp(evaluation);
  }

  function getTreatmentWithConfig(key: SplitIO.SplitKey, featureFlagName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
    return getTreatment(key, featureFlagName, attributes, options, true, GET_TREATMENT_WITH_CONFIG);
  }

  function getTreatments(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, methodName = GET_TREATMENTS) {
    const stopTelemetryTracker = telemetryTracker.trackEval(withConfig ? TREATMENTS_WITH_CONFIG : TREATMENTS);

    const wrapUp = (evaluationResults: Record<string, IEvaluationResult>) => {
      const queue: ImpressionDecorated[] = [];
      const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
      const properties = stringify(options);
      Object.keys(evaluationResults).forEach(featureFlagName => {
        treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
      });
      impressionsTracker.track(queue, attributes);

      stopTelemetryTracker(queue[0] && queue[0].imp.label);
      return treatments;
    };

    const evaluations = readinessManager.isReadyFromCache() ?
      evaluateFeatures(log, key, featureFlagNames, attributes, storage, options) :
      isAsync ? // If the SDK is not ready, treatment may be incorrect due to having splits but not segments data, or storage is not connected
        Promise.resolve(treatmentsNotReady(featureFlagNames)) :
        treatmentsNotReady(featureFlagNames);

    return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations);
  }

  function getTreatmentsWithConfig(key: SplitIO.SplitKey, featureFlagNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
    return getTreatments(key, featureFlagNames, attributes, options, true, GET_TREATMENTS_WITH_CONFIG);
  }

  function getTreatmentsByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions, withConfig = false, method: Method = TREATMENTS_BY_FLAGSETS, methodName = GET_TREATMENTS_BY_FLAG_SETS) {
    const stopTelemetryTracker = telemetryTracker.trackEval(method);

    const wrapUp = (evaluationResults: Record<string, IEvaluationResult>) => {
      const queue: ImpressionDecorated[] = [];
      const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
      const properties = stringify(options);
      Object.keys(evaluationResults).forEach(featureFlagName => {
        treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
      });
      impressionsTracker.track(queue, attributes);

      stopTelemetryTracker(queue[0] && queue[0].imp.label);
      return treatments;
    };

    const evaluations = readinessManager.isReadyFromCache() ?
      evaluateFeaturesByFlagSets(log, key, flagSetNames, attributes, storage, methodName, options) :
      isAsync ?
        Promise.resolve({}) :
        {};

    return thenable(evaluations) ? evaluations.then((res) => wrapUp(res)) : wrapUp(evaluations);
  }

  function getTreatmentsWithConfigByFlagSets(key: SplitIO.SplitKey, flagSetNames: string[], attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
    return getTreatmentsByFlagSets(key, flagSetNames, attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS);
  }

  function getTreatmentsByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
    return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, false, TREATMENTS_BY_FLAGSET, GET_TREATMENTS_BY_FLAG_SET);
  }

  function getTreatmentsWithConfigByFlagSet(key: SplitIO.SplitKey, flagSetName: string, attributes?: SplitIO.Attributes, options?: SplitIO.EvaluationOptions) {
    return getTreatmentsByFlagSets(key, [flagSetName], attributes, options, true, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET);
  }

  // Internal function
  function processEvaluation(
    evaluation: IEvaluationResult,
    featureFlagName: string,
    key: SplitIO.SplitKey,
    properties: string | undefined,
    withConfig: boolean,
    invokingMethodName: string,
    queue: ImpressionDecorated[]
  ): SplitIO.Treatment | SplitIO.TreatmentWithConfig {
    const matchingKey = getMatching(key);
    const bucketingKey = getBucketing(key);

    const { changeNumber, impressionsDisabled } = evaluation;
    let { treatment, label, config = null } = evaluation;

    if (treatment === CONTROL) {
      const fallbackTreatment = fallbackCalculator(featureFlagName, label);
      treatment = fallbackTreatment.treatment;
      label = fallbackTreatment.label;
      config = fallbackTreatment.config;
    }

    if (validateDefinitionExistence(log, readinessManager, featureFlagName, label, invokingMethodName)) {
      log.info(IMPRESSION_QUEUEING, ['Feature flag', featureFlagName, matchingKey, treatment, label]);
      queue.push({
        imp: {
          feature: featureFlagName,
          keyName: matchingKey,
          treatment,
          time: Date.now(),
          bucketingKey,
          label,
          changeNumber: changeNumber as number,
          properties
        },
        disabled: impressionsDisabled
      });
    }

    if (withConfig) {
      return {
        treatment,
        config: config as string | null
      };
    }

    return treatment;
  }

  const track = trackMethodFactory(params);

  return {
    getTreatment,
    getTreatmentWithConfig,
    getTreatments,
    getTreatmentsWithConfig,
    getTreatmentsByFlagSets,
    getTreatmentsWithConfigByFlagSets,
    getTreatmentsByFlagSet,
    getTreatmentsWithConfigByFlagSet,
    track,
  } as SplitIO.IClient | SplitIO.IAsyncClient;
}
