/* eslint-disable unused-imports/no-unused-vars */
/* eslint-disable capitalized-comments */
import { isQueryVariableKey } from "./constants";

/**
 * It's difficult to remove fields from server-returned types (Expression,
 * Split, EventType, CommitConfig, etc) as old SDK versions will depend on them.
 * So we should only add fields to these types if we don't expect to remove them
 * later.
 */

// Value Type

export type ValueType =
  | VoidValueType
  | BooleanValueType
  | IntValueType
  | FloatValueType
  | StringValueType
  | RegexValueType
  | EnumValueType
  | ObjectValueType
  | UnionValueType
  | ListValueType
  | FunctionValueType;

export type VoidValueType = { type: "VoidValueType" };
export type BooleanValueType = { type: "BooleanValueType" };
export type IntValueType = { type: "IntValueType" };
export type FloatValueType = { type: "FloatValueType" };
export type StringValueType = { type: "StringValueType" };
export type RegexValueType = { type: "RegexValueType" };

export type EnumValueType = {
  type: "EnumValueType";
  enumTypeName: string; // Must be an enum type defined in the schema
};

export type ObjectValueType = {
  type: "ObjectValueType";
  objectTypeName: string; // Must be an object type defined in the schema
};

export type UnionValueType = {
  type: "UnionValueType";
  unionTypeName: string; // Must be a union type defined in the schema
};

export type ListValueType = {
  type: "ListValueType";
  itemValueType: ValueType;
};

export type FunctionValueType = {
  type: "FunctionValueType";
  parameterValueTypes: ValueType[];
  returnValueType: ValueType;
};

// Schema

export type Schema = {
  enums: { [enumTypeName: string]: EnumSchema };
  objects: { [objectTypeName: string]: ObjectSchema };
  unions: { [unionTypeName: string]: UnionSchema };
  tags?: { [name: string]: Tag };
};

export type CommonTypeSchema = {
  description: string | null;
};

export type DeprecationTypeSchema = {
  deprecationReason?: string;
};

export type EnumSchema = CommonTypeSchema & {
  values: { [enumValue: string]: EnumValueSchema };
};

export type EnumValueSchema = CommonTypeSchema & DeprecationTypeSchema;

export type ObjectRole = "args" | "input" | "output" | "event";

export type ObjectSchema = CommonTypeSchema & {
  role: ObjectRole;
  fields: { [fieldName: string]: ObjectFieldSchema };
};

export type Tag = {
  name: string;
  color: string;
};

export type ObjectFieldSchema = CommonTypeSchema &
  DeprecationTypeSchema & {
    valueType: ValueType;
  };

export type UnionSchema = CommonTypeSchema & {
  variants: { [objectTypeName: string]: true };
};

// Expression

export type Expression =
  | NoOpExpression
  | BooleanExpression
  | IntExpression
  | FloatExpression
  | StringExpression
  | RegexExpression
  | EnumExpression
  | ObjectExpression
  | GetFieldExpression
  | UpdateObjectExpression
  | ListExpression
  | SwitchExpression
  | EnumSwitchExpression
  | ComparisonExpression
  | ArithmeticExpression
  | RoundNumberExpression
  | StringifyNumberExpression
  | StringConcatExpression // TODO: Break into primitives
  | GetUrlQueryParameterExpression // TODO: Break into primitives
  | SplitExpression // TODO: Break into primitives
  | LogEventExpression
  | FunctionExpression
  | VariableExpression
  | ApplicationExpression;

export type BaseExpressionFields = {
  id: string; // Nano ID
  isTransient?: boolean;
  logs?: Logs;
  metadata?: {
    note?: string;
    permissions?: Permissions;
    tags?: { [tagName: string]: true };
  };
};

export type LogsHandler = (
  logs: Required<
    Pick<Logs, "messageList" | "eventList" | "evaluationList" | "exposureList">
  >
) => void;

export type Logs = {
  messageList?: Message[];
  eventList?: Event[];
  exposureList?: Exposure[];
  evaluationList?: Evaluation[];

  evaluations?: CountMap; // Key: expressionId
  /** @deprecated - use eventList instead */
  events?: CountMap; // Key: stableStringifiedEvent
  /** @deprecated - use exposureList instead */
  exposures?: CountMap; // Key: stableStringifiedExposure
};

export type CountMap = { [key: string]: number };

export type Evaluation = {
  path: string;
  value: Value;
  args: { [path: string]: ObjectValue };
  isFallback: boolean;
};

export type Exposure = {
  splitId: string;
  unitId: string;
  event: Event | null;
  assignment: SplitAssignment;
};

export type Event = {
  objectTypeName: string;
  payload: ObjectValue;
};

export type Message = {
  level: LogLevel;
  message: string;
  metadata: object;
};

export type Permissions = {
  // TODO: Maybe add inherit flag to inherit permissions from the parent
  group: { [groupId: string]: Permission };
  user: { [userId: string]: Permission };
};

export type Permission = {
  // TODO: read
  write: "allow"; // TODO: unset, deny
};

export type NoOpExpression = BaseExpressionFields & {
  type: typeof NoOpExpressionType;
  valueType: VoidValueType;
};
export const NoOpExpressionType = "NoOpExpression" as const;

export type BooleanExpression = BaseExpressionFields & {
  type: typeof BooleanExpressionType;
  valueType: BooleanValueType;
  value: boolean;
};
export const BooleanExpressionType = "BooleanExpression" as const;

export type IntExpression = BaseExpressionFields & {
  type: typeof IntExpressionType;
  valueType: IntValueType;
  value: number;
};
export const IntExpressionType = "IntExpression" as const;

export type FloatExpression = BaseExpressionFields & {
  type: typeof FloatExpressionType;
  valueType: FloatValueType;
  value: number;
};
export const FloatExpressionType = "FloatExpression" as const;

export type StringExpression = BaseExpressionFields & {
  type: typeof StringExpressionType;
  valueType: StringValueType;
  value: string;
};
export const StringExpressionType = "StringExpression" as const;

export type RegexExpression = BaseExpressionFields & {
  type: typeof RegexExpressionType;
  valueType: RegexValueType;
  value: string; // Must be a valid regex
};
export const RegexExpressionType = "RegexExpression" as const;

export type EnumExpression = BaseExpressionFields & {
  type: typeof EnumExpressionType;
  valueType: EnumValueType; // E.g. Language
  value: string; // Must be a valid enum value of the enum given by valueType
};
export const EnumExpressionType = "EnumExpression" as const;

export type ObjectExpression = BaseExpressionFields & {
  type: typeof ObjectExpressionType;
  valueType: ObjectValueType; // E.g. Content
  // Must match valueType.objectTypeName; added explicitly so SDK code doesn't
  // depend on valueType and we can remove it in future, e.g. if we move from
  // explicit type annotations to type inference
  objectTypeName: string;
  // Must contain the field names of all the fields in the schema object
  // objectTypeName. Each field's valueType must match the valueType
  // given by its matching schema field
  fields: { [fieldName: string]: Expression | null };
};
export const ObjectExpressionType = "ObjectExpression" as const;

export type GetFieldExpression = BaseExpressionFields & {
  type: typeof GetFieldExpressionType;
  valueType: ValueType; // E.g. String
  object: Expression | null; // Must have an ObjectValueType valueType
  // Must be a valid field path in the schema object referenced by object's
  // valueType and the valueType of the field must match this expression's
  // valueType
  fieldPath: string | null;
};
export const GetFieldExpressionType = "GetFieldExpression" as const;

export type UpdateObjectExpression = BaseExpressionFields & {
  type: typeof UpdateObjectExpressionType;
  valueType: ObjectValueType; // E.g. Content
  // Its valueType must match this expression's valueType
  object: Expression | null;
  // Each field name must be a valid one in the schema object referenced by
  // valueType.objectValueType and the valueType of its expression must match
  // the valueType of the referenced field
  updates: { [fieldName: string]: Expression | null };
};
export const UpdateObjectExpressionType = "UpdateObjectExpression" as const;

export type ListExpression = BaseExpressionFields & {
  type: typeof ListExpressionType;
  valueType: ListValueType; // E.g. List[BlogPost]
  // Each item's valueType must match valueType.itemValueType
  items: (Expression | null)[];
};
export const ListExpressionType = "ListExpression";

export type SwitchExpression = BaseExpressionFields & {
  type: typeof SwitchExpressionType;
  valueType: ValueType; // E.g. Content
  control: Expression | null;
  cases: {
    id: string;
    // Its valueType must match control's valueType
    when: Expression | null;
    // Its valueType must match this expression's valueType
    then: Expression | null;
  }[];
  // Its valueType must match this expression's valueType
  default: Expression | null;
};
export const SwitchExpressionType = "SwitchExpression" as const;

export type EnumSwitchExpression = BaseExpressionFields & {
  type: typeof EnumSwitchExpressionType;
  valueType: ValueType; // E.g. Content
  control: Expression | null; // Must have an EnumValueType valueType
  // Must contain the enum values of all the enum values in the schema enum
  // matching control.valueType. Each expression's valueType must match this
  // expression's valueType
  cases: { [enumValue: string]: Expression | null };
};
export const EnumSwitchExpressionType = "EnumSwitchExpression" as const;

const comparisonOperators = [
  "==",
  "!=",
  "<",
  "<=",
  ">",
  ">=",
  "AND",
  "OR",
  "in",
  "notIn",
  "startsWith",
  "notStartsWith",
  "endsWith",
  "notEndsWith",
  "contains",
  "notContains",
  "matches",
  "notMatches",
] as const;

export type ComparisonOperator = (typeof comparisonOperators)[number];

export type ComparisonExpression = BaseExpressionFields & {
  type: typeof ComparisonExpressionType;
  valueType: BooleanValueType;
  operator: ComparisonOperator | null;
  a: Expression | null; // Must be compatible with operator and b
  b: Expression | null; // Must be compatible with operator and a
};
export const ComparisonExpressionType = "ComparisonExpression";

export const arithmeticOperators = ["+", "-", "*", "/", "POW", "MOD"] as const;

export type ArithmeticOperator = (typeof arithmeticOperators)[number];

export type ArithmeticExpression = BaseExpressionFields & {
  type: typeof ArithmeticExpressionType;
  valueType: IntValueType | FloatValueType;
  operator: ArithmeticOperator | null;
  // The a and b expressions can only have an IntValueType valueType if this
  // expression does else they can have a FloatValueType valueType too
  a: Expression | null;
  b: Expression | null;
};
export const ArithmeticExpressionType = "ArithmeticExpression" as const;

export type RoundNumberExpression = BaseExpressionFields & {
  type: typeof RoundNumberExpressionType;
  valueType: IntValueType;
  // Must have a FloatValueType (or IntValueType) valueType
  number: Expression | null;
};
export const RoundNumberExpressionType = "RoundNumberExpression" as const;

export type StringifyNumberExpression = BaseExpressionFields & {
  type: typeof StringifyNumberExpressionType;
  valueType: StringValueType;
  // Must have a FloatValueType (or IntValueType) valueType
  number: Expression | null;
};
export const StringifyNumberExpressionType =
  "StringifyNumberExpression" as const;

export type StringConcatExpression = BaseExpressionFields & {
  type: typeof StringConcatExpressionType;
  valueType: StringValueType;
  strings: Expression | null; // Must have a List[String] valueType
};
export const StringConcatExpressionType = "StringConcatExpression" as const;

export type GetUrlQueryParameterExpression = BaseExpressionFields & {
  type: typeof GetUrlQueryParameterExpressionType;
  valueType: StringValueType;
  url: Expression | null; // Must have a String valueType
  queryParameterName: Expression | null; // Must have a String valueType
};
export const GetUrlQueryParameterExpressionType =
  "GetUrlQueryParameterExpression" as const;

// Evaluating this expression will log an exposure for the given <unitId, armId>
// if !!expose
export type SplitExpression = BaseExpressionFields & {
  type: typeof SplitExpressionType;
  valueType: ValueType; // E.g. Content
  splitId: string | null; // Must be a valid split ID
  dimensionId: string | null; // Must be a dimension ID in the selected split
  expose: Expression | null; // Must have a Boolean valueType
  unitId: Expression | null; // Must have a String valueType
  // The dimension mapping type must match the dimension type
  dimensionMapping: DimensionMapping;
  eventObjectTypeName: string | null; // Must match the object value type name of the payload.
  eventPayload: Expression | null; // Must have Object valueType

  // @deprecated - use payload instead.
  featuresMapping: FeaturesMapping;
};
export const SplitExpressionType = "SplitExpression" as const;

export type DimensionMapping =
  | DiscreteDimensionMapping
  | ContinuousDimensionMapping;

export type DiscreteDimensionMapping = {
  type: typeof DiscreteDimensionType;
  // Must contain all the arm IDs in the selected (discrete) dimension as well
  // as a default arm if needed and each arm expression's valueType must match
  // this expression's valueType
  cases: { [armId: string]: Expression | null };
};
export const DiscreteDimensionType = "discrete";

export type FeaturesMapping = { [featureId: string]: Expression | null };

export type ContinuousDimensionMapping = {
  type: typeof ContinuousDimensionType;
  // Must have a FunctionValueType valueType with a single Float parameter
  // type and a return type that matches this expression's valueType
  function: Expression | null;
};
export const ContinuousDimensionType = "continuous";

// Evaluating this expression will log an event <eventTypeId, unitId>
export type LogEventExpression = BaseExpressionFields & {
  type: typeof LogEventExpressionType;
  valueType: VoidValueType;
  eventObjectTypeName: string | null; // Must match the object value type name of the payload.
  eventPayload: Expression | null; // Must have Object valueType

  // @deprecated - use payload instead.
  eventTypeId: string | null; // Must be a valid event type ID
  // @deprecated - use payload instead.
  unitId: Expression | null; // Must have a String valueType
  // @deprecated - use payload instead.
  featuresMapping: FeaturesMapping;
};
export const LogEventExpressionType = "LogEventExpression" as const;

export type FunctionExpression = BaseExpressionFields & {
  type: typeof FunctionExpressionType;
  valueType: FunctionValueType; // E.g. (Int, Int) -> Int
  // Must be of length valueType.parameterValueTypes.length
  parameters: Parameter[];
  // This expression and its descendants can reference the parameters as
  // variables. Its valueType must match the returnValueType of this
  // expression's valueType
  body: Expression | null;
};
export const FunctionExpressionType = "FunctionExpression" as const;

export type Parameter = {
  id: string; // Nano ID
  name: string;
};

export type VariableExpression = BaseExpressionFields & {
  type: typeof VariableExpressionType;
  valueType: ValueType;
  // The valueType of the variable must match this expression's valueType
  variableId: string;
};
export const VariableExpressionType = "VariableExpression" as const;

export type ApplicationExpression = BaseExpressionFields & {
  type: typeof ApplicationExpressionType;
  valueType: ValueType;
  // Must have a FunctionValueType valueType with a returnValueType that matches
  // this expression's valueType
  function: Expression | null;
  // Must contain an argument for each function parameter and each argument's
  // valueType must match its parameter's valueType
  arguments: (Expression | null)[];
};
export const ApplicationExpressionType = "ApplicationExpression" as const;

// Split

export type SplitType = "test" | "ml";

export type SplitBase = {
  id: string; // Nano ID
  name: string;
  description?: string;
  archived?: boolean;
  dimensions: { [dimensionId: string]: Dimension };
  eventObjectTypeName: string | null; // refers to event object schema definition
  // @deprecated - use payload instead
  featureIds: { [featureId: string]: true };
};

export type TestSplit = SplitBase & {
  type: "test";
};

export type MLSplit = SplitBase & {
  type: "ml";
  rewardEvents: {
    eventObjectTypeName: string; // refers to event object schema definition
    unitIdPayloadPath: string[]; // path to unit id in the payload of the goal event type
  }[];
  // E.g. LEAST(COUNT(*) FILTER (WHERE event_object_type_name = 'ClickEvent'), 1)
  // TODO: Develop abstraction over raw SQL
  rewardSql: string;
};

export type Split = TestSplit | MLSplit;

export type SplitMap = { [splitId: string]: Split };

export type Dimension = DiscreteDimension | ContinuousDimension;

type BaseDimensionFields = {
  id: string; // Nano ID
  splitId: string;
  index: number;
  name: string;
};

export type DiscreteDimension = BaseDimensionFields & {
  type: typeof DiscreteDimensionType;
  arms: { [armId: string]: Arm };
  controlArmId?: string;
};

export type Arm = {
  id: string; // Nano ID
  dimensionId: string;
  index: number;
  name: string;
  allocation: number; // E.g. 0.5; only used for test splits
  description?: string;
};

type ContinuousDimension = BaseDimensionFields & {
  type: typeof ContinuousDimensionType;
  range: number[][]; // E.g. [ [-10, -5), [0, 1.3) , [1.5, 2) ]
};

// Event Type
// @deprecated - use object with role event instead
export type EventType = {
  id: string; // Nano ID
  name: string;
  // @deprecated - use payload instead
  featureIds: { [featureId: string]: true };
};
// @deprecated
export type EventTypeMap = { [eventTypeId: string]: EventType };

// Feature
// @deprecated - use event and split payloads instead
export type Feature = {
  id: string; // Nano ID
  name: string;
  valueType: ValueType;
};
// @deprecated
export type FeatureMap = { [featureId: string]: Feature };

// Commit

export type BaseCommitData = {
  schemaCode: string;
  expression: Expression;
  splits: SplitMap;
  eventTypes: EventTypeMap;
  features: FeatureMap;
};

export type CommitData = BaseCommitData & {
  id: number;
  projectId: number;
  config: CommitConfig;
  hash: string;
};

export type ActiveCommitData = CommitData;

// Commit Config

export type CommitConfig = {
  splitConfig: { [splitId: string]: SplitConfig };
};

export type SplitConfig = EpsilonGreedyConfig | PersonalizationSplitConfig;

export type EpsilonGreedyConfig = {
  type: "EpsilonGreedyConfig";
  epsilon: number; // Between 0 and 1
  bestAssignment: SplitAssignment;
};

type PersonalizationSplitConfig = {
  type: "PersonalizationSplitConfig";
  epsilon: number; // Between 0 and 1
  logic: {
    [dimensionId: string]: {
      rules: {
        featureValuesPath: string[];
        featureValue: Value;
        armId: string;
      }[];
      defaultArmId: string;
    };
  };
};

// Query

export type Query<
  TFieldArguments extends ObjectValueWithVariables | ObjectExpression,
> = {
  name?: string;
  variableDefinitions: VariableDefinitions;
  fragmentDefinitions: FragmentDefinitions<TFieldArguments>;
  fieldQuery: FieldQuery<TFieldArguments>;
};

export type FragmentDefinitions<
  TFieldArguments extends ObjectValueWithVariables | ObjectExpression,
> = {
  [fragmentName: string]: InlineFragment<TFieldArguments>;
};

export type VariableDefinitions = {
  [variableName: string]: { valueType: ValueType; defaultValue?: Value };
};

export type FieldQuery<
  TFieldArguments extends ObjectValueWithVariables | ObjectExpression,
> = {
  [objectTypeName: string]: Fragment<TFieldArguments>;
};

export type Fragment<
  TFieldArguments extends ObjectValueWithVariables | ObjectExpression,
> = InlineFragment<TFieldArguments> | FragmentSpread;

export type InlineFragment<
  TFieldArguments extends ObjectValueWithVariables | ObjectExpression,
> = {
  type: "InlineFragment";
  objectTypeName: string;
  selection: Selection<TFieldArguments>;
};

export type FragmentSpread = {
  type: "FragmentSpread";
  fragmentName: string;
};

export type Selection<
  TFieldArguments extends ObjectValueWithVariables | ObjectExpression,
> = {
  [fieldName: string]: {
    fieldArguments: TFieldArguments;
    fieldQuery: FieldQuery<TFieldArguments> | null;
  };
};

// Value

export type Value = boolean | number | string | ObjectValue | Value[];

// Any fieldName starting with __ is reserved for internal use
export type ObjectValue = { [fieldName: string]: Value };

export type QueryVariable = { __isVariable: true; name: string };

export function isQueryVariable(
  value: ValueWithVariables
): value is QueryVariable {
  return (
    typeof value === "object" && value !== null && isQueryVariableKey in value
  );
}

export type ValueWithVariables =
  | Value
  | QueryVariable
  | ValueWithVariables[]
  | ObjectValueWithVariables;

export type ObjectValueWithVariables = {
  [fieldName: string]: ValueWithVariables;
};

export type SplitAssignment = {
  [dimensionId: string]: SplitAssignmentEntry;
};

export type SplitAssignmentEntry =
  | { type: typeof DiscreteDimensionType; armId: string }
  | { type: typeof ContinuousDimensionType; value: number };

export type SplitAssignmentWithNullableEntries = {
  [dimensionId: string]: SplitAssignmentEntry | null;
};

export const requestTypes = [
  "codegen",
  "graphql",
  "init",
  "hash",
  "js",
  "schema",
] as const;
export type RequestType = (typeof requestTypes)[number];

// Edge Types

export type SdkType = "js" | "python" | "rust";

export type JsLanguage = "ts" | "js" | "mjs" | "cjs";
export type Language = JsLanguage | "python" | "rust";

export type CodegenFramework =
  | "nextApp"
  | "nextPages"
  | "react"
  | "reactNative"
  | "remix"
  | "gatsby"
  | "vue";

export type CodegenPlatform = "vercel";

export type CodegenRequestBody = {
  query: string | null;
  framework?: CodegenFramework;
  platform?: CodegenPlatform;
  clientFileName?: string;
  getHypertuneImportPath?: string;
  includeToken?: boolean;
  includeFallback?: boolean;
  includeToolbar?: boolean;
  sdkType: SdkType;
  sdkVersion: string;
  language: Language;
};

// QueryObjectValueWithVariables is a concrete instance of
// Query<ObjectValueWithVariables> for compatibility with Zod
export type QueryObjectValueWithVariables = {
  name?: string;
  variableDefinitions: VariableDefinitions;
  fragmentDefinitions: {
    [fragmentName: string]: InlineFragmentObjectValueWithVariables;
  };
  fieldQuery: FieldQueryObjectValueWithVariables;
};

export type FieldQueryObjectValueWithVariables = {
  [objectTypeName: string]: FragmentObjectValueWithVariables;
};

export type FragmentObjectValueWithVariables =
  | InlineFragmentObjectValueWithVariables
  | FragmentSpread;

export type InlineFragmentObjectValueWithVariables = {
  type: "InlineFragment";
  objectTypeName: string;
  selection: SelectionObjectValueWithVariables;
};

export type SelectionObjectValueWithVariables = {
  [fieldName: string]: {
    fieldArguments: ObjectValueWithVariables;
    fieldQuery: FieldQueryObjectValueWithVariables | null;
  };
};

// Compiler check to ensure the two types remain compatible.
const typedQueryToGeneric: Query<ObjectValueWithVariables> =
  {} as QueryObjectValueWithVariables;
const typedGenericQueryToTyped: QueryObjectValueWithVariables =
  {} as Query<ObjectValueWithVariables>;

export type InitQuery =
  | { type: "Query"; query: QueryObjectValueWithVariables | null }
  | { type: "GraphqlQuery"; code: string }
  | { type: "StoredQuery"; id: string };

export type InitRequestBody = {
  // TODO: Remove support for `string` when everyone is using SDK version >= 2.4
  query: string | InitQuery;
  variables: ObjectValue;
  sdkType: SdkType;
  sdkVersion: string;
};

export type GraphqlRequestBody = {
  query: string;
  variables?: ObjectValue;
  schemaVersion?: string;
};

export type SchemaRequestBody = {
  schemaVersion?: string;
  optionalInputTypes?: boolean;
  introspection?: boolean;
};

export type CodegenFile = {
  name: string;
  content: string;
};

export type CodegenResponseBody = {
  files: CodegenFile[];
  messages: Message[];
  /** @deprecated */
  code?: string;
  /** @deprecated */
  frameworkCode?: string;
};

export type InitData = {
  commitId: number;
  hash: string;
  reducedExpression: Expression;
  splits: SplitMap;
  commitConfig: CommitConfig;
};

export type HashData = {
  commitId: number;
  hash: string;
};

// SDK

export type Step = GetFieldStep | GetItemStep;

type GetFieldStep = {
  type: "GetFieldStep";
  fieldName: string;
  fieldArguments: ObjectValue;
};

type GetItemStep = {
  type: "GetItemStep";
  index: number;
  fallbackLength: number;
};

export type UpdateTrigger = "initDataProvider" | "hydration" | "override";

export type UpdateListener = (
  newStateHash: string, // TODO: move into metadata in the next major release.
  metadata: {
    becameReady: boolean;
    updateTrigger: UpdateTrigger;
    hasUpdated: boolean;
  }
) => void;

export type TracedFetch = (
  traceId: string,
  url: string,
  requestInit: Omit<RequestInit, "headers"> & {
    headers: Record<string, string>;
  }
) => Promise<Response>;

export type InitDataProvider = {
  getName: () => string;
  getInitData: GetInitDataFunction;
  getHashData?: GetHashDataFunction;
};

export type GetInitDataFunction = (args: {
  traceId: string;
  initQuery: InitQuery;
  variableValues: ObjectValue;
}) => Promise<InitData>;

export type GetHashDataFunction = (args: {
  traceId: string;
  initQuery: InitQuery;
  variableValues: ObjectValue;
}) => Promise<HashData>;

/**
 * `CreateOptions` contains options used to create the Hypertune source. Options
 * with generated types are defined in your generated code.
 */
export type CreateOptions = {
  /**
   * `branchName` specifies a Hypertune branch to initialize from.
   * This is useful for testing significant logic and schema changes, especially
   * breaking schema changes, as different schema versions can be served at the
   * same time on different branches during a migration.
   */
  branchName?: string;

  /**
   * `initData` specifies initial `InitData` for the SDK, allowing it to be used
   * immediately. This can be supplied via a static build-time snapshot in your
   * generated client, via hydration or even manually (e.g. in unit tests).
   */
  initData?: InitData;
  /**
   * `initDataProvider` specifies where the SDK should fetch its data from.
   *
   * - When set to `null` the SDK won't fetch any initialization data, relying
   * only on a build-time snapshot in your generated client or hydration.
   * - When set to `undefined` it will default to
   * `HypertuneEdgeInitDataProvider`.
   * - For initialization from Vercel Edge Config, set this to a new instance of
   * `VercelEdgeConfigInitDataProvider`.
   *
   * @default HypertuneEdgeInitDataProvider
   */
  initDataProvider?: InitDataProvider | null;

  /**
   * `lastInitDataRefreshTime` is a timestamp specifying the last time
   * the provided `initData` was last checked for freshness using an
   * `InitDataProvider`. It is ignored when no `initData` was provided.
   */
  lastInitDataRefreshTime?: number | null;
  /**
   * `initDataRefreshIntervalMs` specifies the interval, in milliseconds, at
   * which the SDK checks for updates. The minimum allowed value is 1000ms.
   *
   * - When `initIfNeeded` is called, the SDK will make an initialization
   * request only if the last initialization occurred at least this interval
   * ago.
   *
   * - When `shouldRefreshInitData` is `true`, this value controls how often the
   * SDK checks for updates in the background.
   *
   * - When `initDataProvider` is null, the SDK will never check for updates.
   *
   * @default 2000
   */
  initDataRefreshIntervalMs?: number;
  /**
   * `shouldRefreshInitData` specifies whether the SDK checks for flag updates
   * in the background. The frequency of checks is controlled by the
   * `initDataRefreshIntervalMs` option.
   *
   * When `initDataProvider` is `null` the SDK will never check for updates.
   *
   * @default true (false when using VercelEdgeConfigInitDataProvider)
   */
  shouldRefreshInitData?: boolean;
  /**
   * `shouldRefreshInitDataOnCreate` specifies whether the SDK should
   * initialize from `initDataProvider` on creation.
   *
   * When `initDataProvider` is `null` the SDK will never check for updates.
   *
   * @default true (false when using VercelEdgeConfigInitDataProvider)
   */
  shouldRefreshInitDataOnCreate?: boolean;

  /**
   * shouldSkipInitDataUpdateOnRefresh controls whether the SDK skips fetching
   * and updating its internal state when it detects a new commit exists. It
   * still triggers an update notification. This is useful for clients that
   * hydrate from their own servers.
   *
   * @default false
   */
  shouldSkipInitDataUpdateOnRefresh?: boolean;

  /**
   * cacheSize controls the size of internal SDK caches.
   *
   * Setting it to 0 disables caching.
   *
   * @default 250
   */
  cacheSize?: number;

  remoteLogging?: {
    /**
     * `mode` determines how log messages, expression evaluations, split
     * exposures and analytics events are sent to the remote server.
     *
     * - When set to "normal", all data is sent to the remote server.
     *
     * - When set to "off", no data is sent to the remote server. This is useful
     * in development and test environments.
     *
     * - When set to "session", log messages, expression evaluations and
     * split exposures are deduplicated per session, based on the provided
     * context. However, all analytics events are still sent to the remote
     * server.
     *
     * @default "normal" ("session" when the SDK is used in the browser)
     */
    mode?: RemoteLoggingMode;
    /**
     * `endpointUrl` allows you to send SDK log messages, expression
     * evaluations, split exposures and analytics events to your own server. You
     * can then forward them to Hypertune Edge or other analytics or
     * observability systems.
     *
     * @default https://gcp.fasthorse.workers.dev/logs
     */
    endpointUrl?: string;
    /**
     * `flushIntervalMs` specifies the interval, in milliseconds, at which the
     * SDK flushes logs to the remote server. The minimum allowed value is
     * 1000ms.
     *
     * When set to `null`, the SDK will never automatically flush logs, so you
     * you will need to flush logs manually using the `flushLogs` method.
     *
     * When `mode` is set to "off" the SDK will never automatically flush logs.
     *
     * @default 2000
     */
    flushIntervalMs?: number | null;
  };
  /**
   * @deprecated use `logsHandler` instead
   */
  localLogger?: LocalLogger;
  /**
   * By default, all message logs with level `Info` or higher are logged to the
   * console.
   *
   * You can set a `logsHandler` to handle logs yourself and override this
   * behavior, e.g. by logging `Debug` logs to the console too or forwarding
   * logs, including flag evaluations, analytics events and split exposures to
   * other analytics or observability systems.
   */
  logsHandler?: LogsHandler;

  /**
   * @deprecated Use `shouldRefreshInitData` instead.
   */
  shouldCheckForUpdates?: boolean;
  /**
   * @deprecated Use `lastInitDataRefreshTime` instead.
   */
  lastDataProviderInitTime?: number | null;
  /**
   * @deprecated Use `initDataRefreshIntervalMs` and `shouldCheckForUpdates`
   * instead.
   */
  initIntervalMs?: number;
};

export type RemoteLoggingMode = "normal" | "off" | "session";

export type LocalLogger = (
  level: LogLevel,
  message: string,
  metadata: object
) => void;

export type DehydratedState<
  TOverride extends object,
  TVariableValues extends ObjectValue,
> = {
  initData: InitData;
  lastInitDataRefreshTime: number | null;
  override: DeepPartial<TOverride> | null;
  variableValues: TVariableValues;
};

export type DeepPartial<T> =
  | (T extends (infer U)[]
      ? DeepPartial<U>[]
      : T extends object
        ? { [P in keyof T]?: DeepPartial<T[P]> }
        : T)
  | undefined;

// Logs endpoint schema

export type CreateLogsInput = {
  evaluations: EvaluationCountInput[];
  events: EventInput[];
  exposures: ExposureInput[];
  idempotencyKey: string;
  logs: LogInput[];
  token: string;
};

export type LogInput = {
  commitId?: string | null;
  /** A JSON formatted Date string */
  createdAt: string;
  level: LogLevel;
  message: string;
  /** JSON object containing metadata relating to the log */
  metadataJson: string;
  type: LogType;
};

// eslint-disable-next-line no-shadow
export enum LogLevel {
  Debug = "Debug",
  Error = "Error",
  Info = "Info",
  Warn = "Warn",
}

// eslint-disable-next-line no-shadow
export enum LogType {
  /** Codegen requests handled by Hypertune Edge */
  Codegen = "Codegen",
  /** GraphQL requests handled by Hypertune Edge */
  GraphQl = "GraphQL",
  /** Init requests handled by Hypertune Edge */
  Init = "Init",
  /** JS requests handled by Hypertune Edge */
  Js = "JS",
  /** SDK logs that aren't related to a specific Node */
  SdkMessage = "SDKMessage",
  /** SDK logs related to a specific Node */
  SdkNode = "SDKNode",
  /** Schema requests handled by Hypertune Edge */
  // eslint-disable-next-line no-shadow
  Schema = "Schema",
}

export type EvaluationCountInput = {
  commitId: string;
  count: number;
  expressionId: string;
};

export type EventInput = {
  commitId: string;
  createdAt: string;
  eventObjectTypeName?: string | null;
  eventPayloadJson?: string | null;
  eventTypeId?: string | null;
  unitId?: string | null;
};

export type ExposureInput = {
  assignment: AssignmentInput[];
  commitId: string;
  createdAt: string;
  eventObjectTypeName?: string | null;
  eventPayloadJson?: string | null;
  splitId: string;
  unitId: string;
};

export type AssignmentInput = {
  continuousValue?: number | null;
  dimensionId: string;
  discreteArmId?: string | null;
  entryType: DimensionType;
};

// eslint-disable-next-line no-shadow
export enum DimensionType {
  Continuous = "Continuous",
  Discrete = "Discrete",
}
