// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import { InternalAPI } from '@aws-amplify/api/internals';
import { GraphQLAuthMode } from '@aws-amplify/core/internals/utils';

import { ModelInstanceCreator } from './datastore/datastore';
import {
	NAMESPACES,
	extractPrimaryKeyFieldNames,
	isAWSDate,
	isAWSDateTime,
	isAWSEmail,
	isAWSIPAddress,
	isAWSJSON,
	isAWSPhone,
	isAWSTime,
	isAWSTimestamp,
	isAWSURL,
} from './util';
import { PredicateAll } from './predicates';
import { Adapter } from './storage/adapter';

export type Scalar<T> = T extends (infer InnerType)[] ? InnerType : T;

// #region Schema types
/**
 * @deprecated If you intended to use the Schema for `generateClient`, then you've imported the wrong Schema type.
 * Use `import { type Schema } from '../amplify/data/resource' instead. If you intended to import the type for DataStore
 * Schemas, then import "DataStoreSchema" instead.
 */
export type Schema = DataStoreSchema;

export type DataStoreSchema = UserSchema & {
	version: string;
	codegenVersion: string;
};

export interface UserSchema {
	models: SchemaModels;
	nonModels?: SchemaNonModels;
	relationships?: RelationshipType;
	keys?: ModelKeys;
	enums: SchemaEnums;
	modelTopologicalOrdering?: Map<string, string[]>;
}
export interface InternalSchema {
	namespaces: SchemaNamespaces;
	version: string;
	codegenVersion: string;
}
export type SchemaNamespaces = Record<string, SchemaNamespace>;
export type SchemaNamespace = UserSchema & {
	name: string;
};
export type SchemaModels = Record<string, SchemaModel>;
export interface SchemaModel {
	name: string;
	pluralName: string;
	attributes?: ModelAttributes;

	/**
	 * Explicitly defined fields.
	 */
	fields: ModelFields;

	/**
	 * Explicitly defined fields plus implied fields. (E.g., foreign keys.)
	 */
	allFields?: ModelFields;

	syncable?: boolean;
}

/**
 * @private
 * @param obj
 * @returns `true` if the given object likely represents a model in a schema.
 */
export function isSchemaModel(obj: any): obj is SchemaModel {
	return obj && (obj as SchemaModel).pluralName !== undefined;
}

/**
 * @private
 * @param m
 * @returns `true` if the given schema entry defines Schema Model attributes.
 */
export function isSchemaModelWithAttributes(
	m: SchemaModel | SchemaNonModel,
): m is SchemaModel {
	return isSchemaModel(m) && (m as SchemaModel).attributes !== undefined;
}

export type SchemaNonModels = Record<string, SchemaNonModel>;
export interface SchemaNonModel {
	name: string;
	fields: ModelFields;
}
type SchemaEnums = Record<string, SchemaEnum>;
interface SchemaEnum {
	name: string;
	values: string[];
}
export interface ModelMeta<T extends PersistentModel> {
	builder: PersistentModelConstructor<T>;
	schema: SchemaModel;
	pkField: string[];
}
export type ModelAssociation = AssociatedWith | TargetNameAssociation;
interface AssociatedWith {
	connectionType: 'HAS_MANY' | 'HAS_ONE';
	associatedWith: string | string[];
	targetName?: string;
	targetNames?: string[];
}

/**
 * @private
 * @param obj
 * @returns `true` if the object is an `AssociatedWith` definition.
 */
export function isAssociatedWith(obj: any): obj is AssociatedWith {
	return obj && obj.associatedWith;
}

interface TargetNameAssociation {
	connectionType: 'BELONGS_TO';
	targetName?: string;
	targetNames?: string[];
}

/**
 * @private
 * @param obj
 * @returns `true` if the given object specifies either `targetName` or `targetNames`.
 */
export function isTargetNameAssociation(
	obj: any,
): obj is TargetNameAssociation {
	return obj?.targetName || obj?.targetNames;
}

interface FieldAssociation {
	connectionType: 'HAS_ONE' | 'BELONGS_TO' | 'HAS_MANY';
}

/**
 * @private
 * @param obj
 * @param fieldName
 * @returns Truthy if the object has a `FieldAssociation` for the given `fieldName`.
 */
export function isFieldAssociation(
	obj: any,
	fieldName: string,
): obj is FieldAssociation {
	return obj?.fields[fieldName]?.association?.connectionType;
}

export type ModelAttributes = ModelAttribute[];
export interface ModelAttribute {
	type: string;
	properties?: Record<string, any>;
}

export interface ModelAuthRule {
	allow: string;
	provider?: string;
	operations?: string[];
	ownerField?: string;
	identityClaim?: string;
	groups?: string[];
	groupClaim?: string;
	groupsField?: string;
}

export interface ModelAttributeAuth {
	type: 'auth';
	properties: {
		rules: ModelAuthRule[];
	};
}

/**
 * @private
 * @param attr
 * @returns `true` if the given attribute is an auth attribute with rules.
 */
export function isModelAttributeAuth(
	attr: ModelAttribute,
): attr is ModelAttributeAuth {
	return (
		attr.type === 'auth' &&
		attr.properties &&
		attr.properties.rules &&
		attr.properties.rules.length > 0
	);
}

interface ModelAttributeKey {
	type: 'key';
	properties: {
		name?: string;
		fields: string[];
	};
}

interface ModelAttributePrimaryKey {
	type: 'key';
	properties: {
		name: never;
		fields: string[];
	};
}

interface ModelAttributeCompositeKey {
	type: 'key';
	properties: {
		name: string;
		fields: [string, string, string, string?, string?];
	};
}

/**
 * @private
 * @param attr
 * @returns `true` if the given attribute is a key field.
 */
export function isModelAttributeKey(
	attr: ModelAttribute,
): attr is ModelAttributeKey {
	return (
		attr.type === 'key' &&
		attr.properties &&
		attr.properties.fields &&
		attr.properties.fields.length > 0
	);
}

/**
 * @private
 * @param attr
 * @returns `true` if the given attribute is a primary key, indicated by the key being unnamed.
 */
export function isModelAttributePrimaryKey(
	attr: ModelAttribute,
): attr is ModelAttributePrimaryKey {
	return isModelAttributeKey(attr) && attr.properties.name === undefined;
}

/**
 * @private
 * @param attr
 * @returns `true` if the given attribute represents a composite key with multiple fields.
 */
export function isModelAttributeCompositeKey(
	attr: ModelAttribute,
): attr is ModelAttributeCompositeKey {
	return (
		isModelAttributeKey(attr) &&
		attr.properties.name !== undefined &&
		attr.properties.fields.length > 2
	);
}

export interface ModelAttributeAuthProperty {
	allow: ModelAttributeAuthAllow;
	identityClaim?: string;
	groupClaim?: string;
	groups?: string[];
	operations?: string[];
	ownerField?: string;
	provider?: ModelAttributeAuthProvider;
}

export enum ModelAttributeAuthAllow {
	CUSTOM = 'custom',
	OWNER = 'owner',
	GROUPS = 'groups',
	PRIVATE = 'private',
	PUBLIC = 'public',
}

export enum ModelAttributeAuthProvider {
	FUNCTION = 'function',
	USER_POOLS = 'userPools',
	OIDC = 'oidc',
	IAM = 'iam',
	API_KEY = 'apiKey',
}

export type ModelFields = Record<string, ModelField>;
export enum GraphQLScalarType {
	ID,
	String,
	Int,
	Float,
	Boolean,
	AWSDate,
	AWSTime,
	AWSDateTime,
	AWSTimestamp,
	AWSEmail,
	AWSJSON,
	AWSURL,
	AWSPhone,
	AWSIPAddress,
}

// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace GraphQLScalarType {
	export function getJSType(
		scalar: keyof Omit<
			typeof GraphQLScalarType,
			'getJSType' | 'getValidationFunction'
		>,
	) {
		switch (scalar) {
			case 'Boolean':
				return 'boolean';
			case 'ID':
			case 'String':
			case 'AWSDate':
			case 'AWSTime':
			case 'AWSDateTime':
			case 'AWSEmail':
			case 'AWSURL':
			case 'AWSPhone':
			case 'AWSIPAddress':
				return 'string';
			case 'Int':
			case 'Float':
			case 'AWSTimestamp':
				return 'number';
			case 'AWSJSON':
				return 'object';
			default:
				throw new Error('Invalid scalar type');
		}
	}

	export function getValidationFunction(
		scalar: keyof Omit<
			typeof GraphQLScalarType,
			'getJSType' | 'getValidationFunction'
		>,
	): ((val: string) => boolean) | ((val: number) => boolean) | undefined {
		switch (scalar) {
			case 'AWSDate':
				return isAWSDate;
			case 'AWSTime':
				return isAWSTime;
			case 'AWSDateTime':
				return isAWSDateTime;
			case 'AWSTimestamp':
				return isAWSTimestamp;
			case 'AWSEmail':
				return isAWSEmail;
			case 'AWSJSON':
				return isAWSJSON;
			case 'AWSURL':
				return isAWSURL;
			case 'AWSPhone':
				return isAWSPhone;
			case 'AWSIPAddress':
				return isAWSIPAddress;
			default:
				return undefined;
		}
	}
}

export interface AuthorizationRule {
	identityClaim: string;
	ownerField: string;
	provider: 'userPools' | 'oidc' | 'iam' | 'apiKey';
	groupClaim: string;
	groups: [string];
	groupsField: string;
	authStrategy: 'owner' | 'groups' | 'private' | 'public';
	areSubscriptionsPublic: boolean;
}

/**
 * @private
 * @returns `true` if the given field specifies a scalar type.
 */
export function isGraphQLScalarType(
	obj: any,
): obj is keyof Omit<
	typeof GraphQLScalarType,
	'getJSType' | 'getValidationFunction'
> {
	return obj && GraphQLScalarType[obj] !== undefined;
}

export interface ModelFieldType {
	model: string;
	modelConstructor?: ModelMeta<PersistentModel>;
}

/**
 * @private
 * @param obj
 * @returns `true` if the given field specifies a Model.
 */
export function isModelFieldType<_ extends PersistentModel>(
	obj: any,
): obj is ModelFieldType {
	const modelField: keyof ModelFieldType = 'model';
	if (obj && obj[modelField]) return true;

	return false;
}

export interface NonModelFieldType {
	nonModel: string;
}

/**
 * @private
 * @param obj
 * @returns `true` if the given field specifies a custom non-model type.
 */
export function isNonModelFieldType(obj: any): obj is NonModelFieldType {
	const typeField: keyof NonModelFieldType = 'nonModel';
	if (obj && obj[typeField]) return true;

	return false;
}

interface EnumFieldType {
	enum: string;
}

/**
 * @private
 * @param obj
 * @returns `true` if the object is an `EnumFieldType`.
 */
export function isEnumFieldType(obj: any): obj is EnumFieldType {
	const modelField: keyof EnumFieldType = 'enum';
	if (obj && obj[modelField]) return true;

	return false;
}

export interface ModelField {
	name: string;
	type:
		| keyof Omit<
				typeof GraphQLScalarType,
				'getJSType' | 'getValidationFunction'
		  >
		| ModelFieldType
		| NonModelFieldType
		| EnumFieldType;
	isArray: boolean;
	isRequired?: boolean;
	isReadOnly?: boolean;
	isArrayNullable?: boolean;
	association?: ModelAssociation;
	attributes?: ModelAttributes[];
}
// #endregion

// #region Model definition
export type NonModelTypeConstructor<T> = new (init: T) => T;

// Class for model
export interface PersistentModelConstructor<T extends PersistentModel> {
	new (init: ModelInit<T, PersistentModelMetaData<T>>): T;
	copyOf(
		src: T,
		mutator: (draft: MutableModel<T, PersistentModelMetaData<T>>) => void,
	): T;
}

/**
 * @private
 * Internal use of Amplify only.
 *
 * Indicates to use lazy models or eager models.
 */
export declare class LazyLoadingDisabled {
	disabled: true;
}

/**
 * @private
 * Internal use of Amplify only.
 *
 * Indicates to use lazy models or eager models.
 */
export declare class LazyLoading {}

export type TypeConstructorMap = Record<
	string,
	PersistentModelConstructor<any> | NonModelTypeConstructor<unknown>
>;

/**
 * Each identifier type is represented using nominal types, see:
 * https://basarat.gitbook.io/typescript/main-1/nominaltyping
 */
export declare const __identifierBrand__: unique symbol;
export type IdentifierBrand<T, K> = T & { [__identifierBrand__]: K };

interface GenericIdentifier {
	field: any;
}

// datastore generates a uuid for you
export type ManagedIdentifier<T, F extends keyof T> = IdentifierBrand<
	{ field: F extends string ? F : never; type: T },
	'ManagedIdentifier'
>;

// you can provide a value, if not, datastore generates a uuid for you
export type OptionallyManagedIdentifier<T, F extends keyof T> = IdentifierBrand<
	{ field: F extends string ? F : never; type: T },
	'OptionallyManagedIdentifier'
>;

// You provide the values
export type CompositeIdentifier<T, K extends (keyof T)[]> = IdentifierBrand<
	{ fields: K; type: T },
	'CompositeIdentifier'
>;

// You provide the value
export type CustomIdentifier<T, K extends keyof T> = CompositeIdentifier<
	T,
	[K]
>;

export type Identifier<T> =
	| ManagedIdentifier<T, any>
	| OptionallyManagedIdentifier<T, any>
	| CompositeIdentifier<T, any>
	| CustomIdentifier<T, any>;

export type IdentifierFields<
	T extends PersistentModel,
	M extends PersistentModelMetaData<T> = never,
> = (MetadataOrDefault<T, M>['identifier'] extends
	| ManagedIdentifier<any, any>
	| OptionallyManagedIdentifier<any, any>
	? MetadataOrDefault<T, M>['identifier']['field']
	: MetadataOrDefault<T, M>['identifier'] extends CompositeIdentifier<
				T,
				infer B
		  >
		? B[number] // B[number]
		: MetadataOrDefault<T, M>['identifier'] extends GenericIdentifier
			? MetadataOrDefault<T, M>['identifier']['field']
			: unknown) &
	string;

export type IdentifierFieldsForInit<
	T extends PersistentModel,
	M extends PersistentModelMetaData<T>,
> = MetadataOrDefault<T, M>['identifier'] extends
	| DefaultPersistentModelMetaData
	| ManagedIdentifier<T, any>
	? never
	: MetadataOrDefault<T, M>['identifier'] extends OptionallyManagedIdentifier<
				T,
				any
		  >
		? IdentifierFields<T, M>
		: MetadataOrDefault<T, M>['identifier'] extends CompositeIdentifier<T, any>
			? IdentifierFields<T, M>
			: never;

// Instance of model
export declare const __modelMeta__: unique symbol;

export interface PersistentModelMetaData<T> {
	identifier?: Identifier<T>;
	readOnlyFields?: string;
}

export interface AsyncCollection<T> extends AsyncIterable<T> {
	toArray(options?: { max?: number }): Promise<T[]>;
}

export type SettableFieldType<T> =
	T extends Promise<infer InnerPromiseType>
		? undefined extends InnerPromiseType
			? InnerPromiseType | null
			: InnerPromiseType
		: T extends AsyncCollection<infer InnerCollectionType>
			? InnerCollectionType[] | undefined
			: undefined extends T
				? T | null
				: T;

export type PredicateFieldType<T> = NonNullable<
	Scalar<
		T extends Promise<infer InnerPromiseType>
			? InnerPromiseType
			: T extends AsyncCollection<infer InnerCollectionType>
				? InnerCollectionType
				: T
	>
>;

type KeysOfType<T, FilterType> = {
	[P in keyof T]: T[P] extends FilterType ? P : never;
}[keyof T];

type KeysOfSuperType<T, FilterType> = {
	[P in keyof T]: FilterType extends T[P] ? P : never;
}[keyof T];

type OptionalRelativesOf<T> =
	| KeysOfType<T, AsyncCollection<any>>
	| KeysOfSuperType<T, Promise<undefined>>;

type OmitOptionalRelatives<T> = Omit<T, OptionalRelativesOf<T>>;
type PickOptionalRelatives<T> = Pick<T, OptionalRelativesOf<T>>;

export interface DefaultPersistentModelMetaData {
	identifier: ManagedIdentifier<{ id: string }, 'id'>;
	readOnlyFields: never;
}

export type MetadataOrDefault<
	T extends PersistentModel,
	_ extends PersistentModelMetaData<T> = never,
> = T extends {
	[__modelMeta__]: PersistentModelMetaData<T>;
}
	? T[typeof __modelMeta__]
	: DefaultPersistentModelMetaData;

export type PersistentModel = Readonly<Record<string, any>>;

export type MetadataReadOnlyFields<
	T extends PersistentModel,
	M extends PersistentModelMetaData<T>,
> = Extract<
	MetadataOrDefault<T, M>['readOnlyFields'] | M['readOnlyFields'],
	keyof T
>;

// This type omits the metadata field in the constructor init object
// This type omits identifier fields in the constructor init object
// This type omits readOnlyFields in the constructor init object
// This type requires some identifiers in the constructor init object (e.g. CustomIdentifier)
// This type makes optional some identifiers in the constructor init object (e.g. OptionallyManagedIdentifier)
export type ModelInitBase<
	T extends PersistentModel,
	// eslint-disable-next-line @typescript-eslint/no-empty-object-type
	M extends PersistentModelMetaData<T> = {},
> = Omit<
	T,
	typeof __modelMeta__ | IdentifierFields<T, M> | MetadataReadOnlyFields<T, M>
> &
	(MetadataOrDefault<T, M>['identifier'] extends OptionallyManagedIdentifier<
		T,
		any
	>
		? Partial<Pick<T, IdentifierFieldsForInit<T, M>>>
		: Required<Pick<T, IdentifierFieldsForInit<T, M>>>);

export type ModelInit<
	T extends PersistentModel,
	// eslint-disable-next-line @typescript-eslint/no-empty-object-type
	M extends PersistentModelMetaData<T> = {},
> = {
	[P in keyof OmitOptionalRelatives<ModelInitBase<T, M>>]: SettableFieldType<
		ModelInitBase<T, M>[P]
	>;
} & {
	[P in keyof PickOptionalRelatives<ModelInitBase<T, M>>]+?: SettableFieldType<
		ModelInitBase<T, M>[P]
	>;
};

type DeepWritable<T> = {
	-readonly [P in keyof T]: T[P] extends TypeName<T[P]>
		? T[P]
		: T[P] extends Promise<infer InnerPromiseType>
			? undefined extends InnerPromiseType
				? InnerPromiseType | null
				: InnerPromiseType
			: T[P] extends AsyncCollection<infer InnerCollectionType>
				? InnerCollectionType[] | undefined | null
				: DeepWritable<T[P]>;
};

export type MutableModel<
	T extends PersistentModel,
	// eslint-disable-next-line @typescript-eslint/no-empty-object-type
	M extends PersistentModelMetaData<T> = {},
	// This provides Intellisense with ALL of the properties, regardless of read-only
	// but will throw a linting error if trying to overwrite a read-only property
> = DeepWritable<
	Omit<T, IdentifierFields<T, M> | MetadataReadOnlyFields<T, M>>
> &
	Readonly<Pick<T, IdentifierFields<T, M> | MetadataReadOnlyFields<T, M>>>;

export interface ModelInstanceMetadata {
	_version: number;
	_lastChangedAt: number;
	_deleted: boolean;
}

export type IdentifierFieldValue<
	T extends PersistentModel,
	M extends PersistentModelMetaData<T>,
> =
	MetadataOrDefault<T, M>['identifier'] extends CompositeIdentifier<T, any>
		? MetadataOrDefault<T, M>['identifier']['fields'] extends [any]
			? T[MetadataOrDefault<T, M>['identifier']['fields'][0]]
			: never
		: MetadataOrDefault<T, M>['identifier'] extends GenericIdentifier
			? T[MetadataOrDefault<T, M>['identifier']['field']]
			: unknown;

export type IdentifierFieldOrIdentifierObject<
	T extends PersistentModel,
	M extends PersistentModelMetaData<T>,
> = Pick<T, IdentifierFields<T, M>> | IdentifierFieldValue<T, M>;

/**
 * @private
 * @param obj
 * @param modelDefinition
 * @returns `true` if the given item is an object that has all identifier fields populated.
 */
export function isIdentifierObject<T extends PersistentModel>(
	obj: any,
	modelDefinition: SchemaModel,
): obj is IdentifierFields<T extends PersistentModel ? T : never, any> {
	const keys = extractPrimaryKeyFieldNames(modelDefinition);

	return (
		typeof obj === 'object' && obj && keys.every(k => obj[k] !== undefined)
	);
}
// #endregion

// #region Subscription messages
export enum OpType {
	INSERT = 'INSERT',
	UPDATE = 'UPDATE',
	DELETE = 'DELETE',
}

export type SubscriptionMessage<T extends PersistentModel> = Pick<
	InternalSubscriptionMessage<T>,
	'opType' | 'element' | 'model' | 'condition'
>;

export interface InternalSubscriptionMessage<T extends PersistentModel> {
	opType: OpType;
	element: T;
	model: PersistentModelConstructor<T>;
	condition: PredicatesGroup<T> | null;
	savedElement?: T;
}

export interface DataStoreSnapshot<T extends PersistentModel> {
	items: T[];
	isSynced: boolean;
}
// #endregion

// #region Predicates

export type PredicateExpression<M extends PersistentModel, FT> =
	TypeName<FT> extends keyof MapTypeToOperands<FT>
		? (
				operator: keyof MapTypeToOperands<FT>[TypeName<FT>],
				// make the operand type match the type they're trying to filter on
				operand: MapTypeToOperands<FT>[TypeName<FT>][keyof MapTypeToOperands<FT>[TypeName<FT>]],
			) => ModelPredicate<M>
		: never;

interface EqualityOperators<T> {
	ne: T;
	eq: T;
}
type ScalarNumberOperators<T> = EqualityOperators<T> & {
	le: T;
	lt: T;
	ge: T;
	gt: T;
};
type NumberOperators<T> = ScalarNumberOperators<T> & {
	between: [T, T];
	in: T[];
	notIn: T[];
};
type StringOperators<T> = ScalarNumberOperators<T> & {
	beginsWith: T;
	contains: T;
	notContains: T;
	in: T[];
	notIn: T[];
};
type BooleanOperators<T> = EqualityOperators<T> & {
	in: T[];
	notIn: T[];
};
interface ArrayOperators<T> {
	contains: T;
	notContains: T;
}
export type AllOperators = NumberOperators<any> &
	StringOperators<any> &
	ArrayOperators<any>;

interface MapTypeToOperands<T> {
	number: NumberOperators<NonNullable<T>>;
	string: StringOperators<NonNullable<T>>;
	boolean: BooleanOperators<NonNullable<T>>;
	'number[]': ArrayOperators<number>;
	'string[]': ArrayOperators<string>;
	'boolean[]': ArrayOperators<boolean>;
}

type TypeName<T> = T extends string
	? 'string'
	: T extends number
		? 'number'
		: T extends boolean
			? 'boolean'
			: T extends string[]
				? 'string[]'
				: T extends number[]
					? 'number[]'
					: T extends boolean[]
						? 'boolean[]'
						: never;

export interface PredicateGroups<T extends PersistentModel> {
	and(
		predicate: (predicate: ModelPredicate<T>) => ModelPredicate<T>,
	): ModelPredicate<T>;
	or(
		predicate: (predicate: ModelPredicate<T>) => ModelPredicate<T>,
	): ModelPredicate<T>;
	not(
		predicate: (predicate: ModelPredicate<T>) => ModelPredicate<T>,
	): ModelPredicate<T>;
}

export type ModelPredicate<M extends PersistentModel> = {
	[K in keyof M]-?: PredicateExpression<M, NonNullable<M[K]>>;
} & PredicateGroups<M>;

export type ProducerModelPredicate<M extends PersistentModel> = (
	condition: ModelPredicate<M>,
) => ModelPredicate<M>;

export interface PredicatesGroup<T extends PersistentModel> {
	type: keyof PredicateGroups<T>;
	predicates: (PredicateObject<T> | PredicatesGroup<T>)[];
}

/**
 * @private
 * @param obj
 * @returns `true` if the given predicate field object, specifying an [in-]equality test against a field.
 */
export function isPredicateObj<T extends PersistentModel>(
	obj: any,
): obj is PredicateObject<T> {
	return obj && (obj as PredicateObject<T>).field !== undefined;
}

/**
 * @private
 * @param obj
 * @returns `true` if the given predicate object is a "group" like "and", "or", or "not".
 */
export function isPredicateGroup<T extends PersistentModel>(
	obj: any,
): obj is PredicatesGroup<T> {
	return obj && (obj as PredicatesGroup<T>).type !== undefined;
}

export interface PredicateObject<T extends PersistentModel> {
	field: keyof T;
	operator: keyof AllOperators;
	operand: any;
}

export enum QueryOne {
	FIRST,
	LAST,
}
export type GraphQLField = Record<
	string,
	Record<string, string | number | [number, number] | string[] | number[]>
>;

export type GraphQLCondition = Partial<
	| GraphQLField
	| {
			and: [GraphQLCondition];
			or: [GraphQLCondition];
			not: GraphQLCondition;
	  }
>;

export type GraphQLFilter = Partial<
	| GraphQLField
	| {
			and: GraphQLFilter[];
	  }
	| {
			or: GraphQLFilter[];
	  }
	| {
			not: GraphQLFilter;
	  }
>;

// #endregion

// #region Pagination

export interface ProducerPaginationInput<T extends PersistentModel> {
	sort?: ProducerSortPredicate<T>;
	limit?: number;
	page?: number;
}

export type ObserveQueryOptions<T extends PersistentModel> = Pick<
	ProducerPaginationInput<T>,
	'sort'
>;

export interface PaginationInput<T extends PersistentModel> {
	sort?: SortPredicate<T>;
	limit?: number;
	page?: number;
}

export type ProducerSortPredicate<M extends PersistentModel> = (
	condition: SortPredicate<M>,
) => SortPredicate<M>;

export type SortPredicate<T extends PersistentModel> = {
	[K in keyof T]-?: SortPredicateExpression<T, NonNullable<T[K]>>;
};

export type SortPredicateExpression<M extends PersistentModel, FT> =
	TypeName<FT> extends keyof MapTypeToOperands<FT>
		? (sortDirection: keyof typeof SortDirection) => SortPredicate<M>
		: never;

export enum SortDirection {
	ASCENDING = 'ASCENDING',
	DESCENDING = 'DESCENDING',
}

export type SortPredicatesGroup<T extends PersistentModel> =
	SortPredicateObject<T>[];

export interface SortPredicateObject<T extends PersistentModel> {
	field: keyof T;
	sortDirection: keyof typeof SortDirection;
}

// #endregion

// #region System Components

export interface SystemComponent {
	setUp(
		schema: InternalSchema,
		namespaceResolver: NamespaceResolver,
		modelInstanceCreator: ModelInstanceCreator,
		getModelConstructorByModelName: (
			namsespaceName: NAMESPACES,
			modelName: string,
		) => PersistentModelConstructor<any>,
		appId?: string,
	): Promise<void>;
}

export type NamespaceResolver = (
	modelConstructor: PersistentModelConstructor<any>,
) => string;

export interface ControlMessageType<T> {
	type: T;
	data?: any;
}

// #endregion

// #region Relationship types
export interface RelationType {
	fieldName: string;
	modelName: string;
	relationType: 'HAS_ONE' | 'HAS_MANY' | 'BELONGS_TO';
	targetName?: string;
	targetNames?: string[];
	associatedWith?: string | string[];
}

interface IndexOptions {
	unique?: boolean;
}

export type IndexesType = [string, string[], IndexOptions?][];

export type RelationshipType = Record<
	string,
	{
		indexes: IndexesType;
		relationTypes: RelationType[];
	}
>;

// #endregion

// #region Key type
export interface KeyType {
	primaryKey?: string[];
	compositeKeys?: Set<string>[];
}

export type ModelKeys = Record<string, KeyType>;

// #endregion

// #region DataStore config types
export interface DataStoreConfig {
	DataStore?: {
		authModeStrategyType?: AuthModeStrategyType;
		conflictHandler?: ConflictHandler; // default : retry until client wins up to x times
		errorHandler?(error: SyncError<PersistentModel>): void; // default : logger.warn
		maxRecordsToSync?: number; // merge
		syncPageSize?: number;
		fullSyncInterval?: number;
		syncExpressions?: SyncExpression[];
		authProviders?: AuthProviders;
		storageAdapter?: Adapter;
	};
	authModeStrategyType?: AuthModeStrategyType;
	conflictHandler?: ConflictHandler; // default : retry until client wins up to x times
	errorHandler?(error: SyncError<PersistentModel>): void; // default : logger.warn
	maxRecordsToSync?: number; // merge
	syncPageSize?: number;
	fullSyncInterval?: number;
	syncExpressions?: SyncExpression[];
	authProviders?: AuthProviders;
	storageAdapter?: Adapter;
}

export interface AuthProviders {
	functionAuthProvider(): { token: string } | Promise<{ token: string }>;
}

export enum AuthModeStrategyType {
	DEFAULT = 'DEFAULT',
	MULTI_AUTH = 'MULTI_AUTH',
}

export type AuthModeStrategyReturn =
	| GraphQLAuthMode
	| GraphQLAuthMode[]
	| undefined
	| null;

export interface AuthModeStrategyParams {
	schema: InternalSchema;
	modelName: string;
	operation: ModelOperation;
}

export type AuthModeStrategy = (
	authModeStrategyParams: AuthModeStrategyParams,
) => AuthModeStrategyReturn | Promise<AuthModeStrategyReturn>;

export enum ModelOperation {
	CREATE = 'CREATE',
	READ = 'READ',
	UPDATE = 'UPDATE',
	DELETE = 'DELETE',
}

export type ModelAuthModes = Record<
	string,
	Record<ModelOperation, GraphQLAuthMode[]>
>;

export type SyncExpression = Promise<{
	modelConstructor: any;
	conditionProducer(c?: any): any;
}>;

/*
Adds Intellisense when passing a function | promise that returns a predicate
Or just a predicate. E.g.,

syncExpressions: [
	syncExpression(Post, c => c.rating('gt', 5)),

	OR

	syncExpression(Post, async () => {
		return c => c.rating('gt', 5)
	}),
]
*/

type Option0 = [];
type Option1<T extends PersistentModel> = [V5ModelPredicate<T> | undefined];
type Option<T extends PersistentModel> = Option0 | Option1<T>;

interface Lookup<T extends PersistentModel> {
	0:
		| ModelPredicateExtender<T>
		| Promise<ModelPredicateExtender<T>>
		| typeof PredicateAll
		| Promise<typeof PredicateAll | symbol>;
	1: PredicateInternalsKey | undefined;
}

type ConditionProducer<T extends PersistentModel, A extends Option<T>> = (
	...args: A
) => A['length'] extends keyof Lookup<T> ? Lookup<T>[A['length']] : never;

/**
 * Build an expression that can be used to filter which items of a given Model
 * are synchronized down from the GraphQL service. E.g.,
 *
 * ```ts
 * import { DataStore, syncExpression } from 'aws-amplify/datastore';
 * import { Post, Comment } from './models';
 *
 *
 * DataStore.configure({
 * 	syncExpressions: [
 * 		syncExpression(Post, () => {
 * 			return (post) => post.rating.gt(5);
 * 		}),
 * 		syncExpression(Comment, () => {
 * 			return (comment) => comment.status.eq('active');
 * 		})
 * 	]
 * });
 * ```
 *
 * When DataStore starts syncing, only Posts with `rating > 5` and Comments with
 * `status === 'active'` will be synced down to the user's local store.
 *
 * @param modelConstructor The Model from the schema.
 * @param conditionProducer A function that builds a condition object that can describe how to filter the model.
 * @returns An sync expression object that can be attached to the DataStore `syncExpressions` configuration property.
 */
export async function syncExpression<
	T extends PersistentModel,
	A extends Option<T>,
>(
	modelConstructor: PersistentModelConstructor<T>,
	conditionProducer: ConditionProducer<T, A>,
): Promise<{
	modelConstructor: PersistentModelConstructor<T>;
	conditionProducer: ConditionProducer<T, A>;
}> {
	return {
		modelConstructor,
		conditionProducer,
	};
}

export interface SyncConflict {
	modelConstructor: PersistentModelConstructor<any>;
	localModel: PersistentModel;
	remoteModel: PersistentModel;
	operation: OpType;
	attempts: number;
}

export interface SyncError<T extends PersistentModel> {
	message: string;
	errorType: ErrorType;
	errorInfo?: string;
	recoverySuggestion?: string;
	model?: string;
	localModel: T;
	remoteModel: T;
	process: ProcessName;
	operation: string;
	cause?: Error;
}

export type ErrorType =
	| 'ConfigError'
	| 'BadModel'
	| 'BadRecord'
	| 'Unauthorized'
	| 'Transient'
	| 'Unknown';

export enum ProcessName {
	'sync' = 'sync',
	'mutate' = 'mutate',
	'subscribe' = 'subscribe',
}

export const DISCARD = Symbol('DISCARD');

export type ConflictHandler = (
	conflict: SyncConflict,
) =>
	| Promise<PersistentModel | typeof DISCARD>
	| PersistentModel
	| typeof DISCARD;
export type ErrorHandler = (error: SyncError<PersistentModel>) => void;

export interface DeferredCallbackResolverOptions {
	callback(): void;
	maxInterval?: number;
	errorHandler?(error: string): void;
}

export enum LimitTimerRaceResolvedValues {
	LIMIT = 'LIMIT',
	TIMER = 'TIMER',
}
// #endregion

export interface AmplifyContext {
	InternalAPI: typeof InternalAPI;
}

// #region V5 predicate types

export type MatchableTypes =
	| string
	| string[]
	| number
	| number[]
	| boolean
	| boolean[];

export type AllFieldOperators = keyof AllOperators;

export type NonNeverKeys<T> = {
	[K in keyof T]: T[K] extends never ? never : K;
}[keyof T];

export type WithoutNevers<T> = Pick<T, NonNeverKeys<T>>;

/**
 * A function that accepts a RecursiveModelPrecicate<T>, which it must use to
 * return a final condition.
 *
 * This is used in `DataStore.query()`, `DataStore.observe()`, and
 * `DataStore.observeQuery()` as the second argument. E.g.,
 *
 * ```
 * DataStore.query(MyModel, model => model.field.eq('some value'))
 * ```
 *
 * More complex queries should also be supported. E.g.,
 *
 * ```
 * DataStore.query(MyModel, model => model.and(m => [
 *   m.relatedEntity.or(relative => [
 *     relative.relativeField.eq('whatever'),
 *     relative.relativeField.eq('whatever else')
 *   ]),
 *   m.myModelField.ne('something')
 * ]))
 * ```
 */
export type RecursiveModelPredicateExtender<RT extends PersistentModel> = (
	lambda: RecursiveModelPredicate<RT>,
) => PredicateInternalsKey;

export type RecursiveModelPredicateAggregateExtender<
	RT extends PersistentModel,
> = (lambda: RecursiveModelPredicate<RT>) => PredicateInternalsKey[];

export type RecursiveModelPredicateOperator<RT extends PersistentModel> = (
	predicates: RecursiveModelPredicateAggregateExtender<RT>,
) => PredicateInternalsKey;

export type RecursiveModelPredicateNegation<RT extends PersistentModel> = (
	predicate: RecursiveModelPredicateExtender<RT>,
) => PredicateInternalsKey;

export type RecursiveModelPredicate<RT extends PersistentModel> = {
	[K in keyof RT]-?: PredicateFieldType<RT[K]> extends PersistentModel
		? RecursiveModelPredicate<PredicateFieldType<RT[K]>>
		: ValuePredicate<RT, RT[K]>;
} & {
	or: RecursiveModelPredicateOperator<RT>;
	and: RecursiveModelPredicateOperator<RT>;
	not: RecursiveModelPredicateNegation<RT>;
} & PredicateInternalsKey;

/**
 * A function that accepts a ModelPrecicate<T>, which it must use to return a
 * final condition.
 *
 * This is used as predicates in `DataStore.save()`, `DataStore.delete()`, and
 * DataStore sync expressions.
 *
 * ```
 * DataStore.save(record, model => model.field.eq('some value'))
 * ```
 *
 * Logical operators are supported. But, condtiions are related records are
 * NOT supported. E.g.,
 *
 * ```
 * DataStore.delete(record, model => model.or(m => [
 * 	m.field.eq('whatever'),
 * 	m.field.eq('whatever else')
 * ]))
 * ```
 */
export type ModelPredicateExtender<RT extends PersistentModel> = (
	lambda: V5ModelPredicate<RT>,
) => PredicateInternalsKey;

export type ModelPredicateAggregateExtender<RT extends PersistentModel> = (
	lambda: V5ModelPredicate<RT>,
) => PredicateInternalsKey[];

export type ValuePredicate<
	_RT extends PersistentModel,
	MT extends MatchableTypes,
> = {
	[K in AllFieldOperators]: K extends 'between'
		? (
				inclusiveLowerBound: Scalar<MT>,
				inclusiveUpperBound: Scalar<MT>,
			) => PredicateInternalsKey
		: K extends 'in' | 'notIn'
			? (operand: Scalar<MT>[]) => PredicateInternalsKey
			: (operand: Scalar<MT>) => PredicateInternalsKey;
};

export type V5ModelPredicate<RT extends PersistentModel> = WithoutNevers<{
	[K in keyof RT]-?: PredicateFieldType<RT[K]> extends PersistentModel
		? never
		: ValuePredicate<RT, RT[K]>;
}> & {
	or: ModelPredicateOperator<RT>;
	and: ModelPredicateOperator<RT>;
	not: ModelPredicateNegation<RT>;
} & PredicateInternalsKey;

export type ModelPredicateOperator<RT extends PersistentModel> = (
	predicates: ModelPredicateAggregateExtender<RT>,
) => PredicateInternalsKey;

export type ModelPredicateNegation<RT extends PersistentModel> = (
	predicate: ModelPredicateExtender<RT>,
) => PredicateInternalsKey;

/**
 * A pointer used by DataStore internally to lookup predicate details
 * that should not be exposed on public customer interfaces.
 */
export class PredicateInternalsKey {
	private __isPredicateInternalsKeySentinel = true;
}

// #endregion
