import { Paths, Get } from 'type-fest';
import { Params } from '@feathersjs/feathers';

/**
 * Factory Template context.
 * Provides access to the current generation context.
 *
 * The `this` type within your template's generator functions.
 */
declare class TemplateContext<TTemplate, TContext extends TemplateContext<TTemplate> = any> {
    protected readonly template: FactoryTemplate<TTemplate, TContext>;
    /**
     * Internal state for the getter machine.
     * The structure of this field can be unexpected unless explicitly accessed
     * through the {@link this.get} method.
     * @private
     */
    readonly _state: ContextState<TTemplate>;
    constructor(template: FactoryTemplate<TTemplate, TContext>);
    /**
     * Resolve the value of a template field within the current generator
     * context. Fields are only resolved once per generator context.
     *
     * This ensures that you can safely reference the same field multiple times
     * within the same generation context and from different fields.
     *
     * @example
     * template = ({
     *     firstName: () => faker.person.firstName(),
     *     lastName: () => faker.person.lastName(),
     *
     *     fullName: () => `${this.get('firstName')} ${this.get('lastName')}`,
     *     // -> John Doe
     *
     *     // Functions are only called once, then cached to ensure consistent
     *     // results within the same generation context.
     *     email: () => `${this.get('firstName')}.${this.get('lastName')}@example.com`
     *     // -> John.Doe@example.com,
     * })
     *
     */
    get<TKey extends Paths<TTemplate> & string>(key: TKey): ContextFieldOutcome<Get<TTemplate, TKey>>;
    /**
     * Run the generator function for a given field. This will not cache the
     * result within the current context. Meaning you can call it multiple times
     * within the same generation context and it will always return a new value.
     *
     * This is useful if you want to extend the result of a field from within
     * another field. Do keep in mind that you might want to use it sparingly
     * in case the field has side-effects. E.g. creating new records in the
     * database.
     *
     * @example
     * template = ({
     *     firstName: () => faker.person.firstName(),
     *     lastName: () => faker.person.lastName(),
     *
     *     fullName: () => `${this.get('firstName')} ${this.get('lastName')}`,
     *     // -> John Doe
     *
     *     family: () => [
     *         this.call('fullName'), // -> <New random name>
     *         this.call('fullName'), // -> <New random name>
     *
     *         this.get('fullName'), // -> John Doe
     *     ]
     */
    call<TKey extends Paths<TTemplate> & string>(key: TKey): ContextFieldOutcome<Get<TTemplate, TKey>>;
    /**
     * Wrap any template functions around an array to indicate to Clues.js
     * what parameters are expected. Which is just this class instance.
     *
     * Enables use of the context parameter in arrow functions.
     */
    protected wrapTemplateField(field: unknown): unknown;
    /**
     * Check whether the provided field is a function we should wrap to help
     * Clues.js resolve input types.
     */
    protected shouldWrap(field: unknown): field is Function;
    /**
     * Attempt to resolve the current context state.
     * Used primarily for testing. The internal state does change during
     * resolve and could yield unexpected results.
     * @private
     */
    _resolveState(): Promise<any>;
}
/**
 * Resolver state. May contain some partially resolved template fields.
 */
type ContextState<TTemplate> = {
    [key in keyof TTemplate]: ContextField<TTemplate[key]>;
};
/**
 * Contextualized FactoryTemplate fields.
 * When resolving some fields will get converted to promises,
 * some immediately to their resulting value, etc.
 *
 * Todo: Attempt to infer types that will never get converted to a promise or
 *  function. (sync functions, static values, etc.)
 */
type ContextField<TType> = TType | Promise<TType> | (() => Promise<TType> | TType);
/**
 * After resolving a field, it'll either be it's final type or a promise
 * in cases where there's peer dependencies or the field function actually
 * is async.
 */
type ContextFieldOutcome<TType> = TType extends ContextField<infer T> ? Promise<T> | T : never;

/**
 * Factory boilerplate template.
 * Defines the fields that will be generated when factories are called.
 */
declare class FactoryTemplate<TTemplate, TContext extends TemplateContext<TTemplate> = TemplateContext<TTemplate>> {
    readonly _schema: TemplateSchema<TTemplate, TContext>;
    constructor(_schema: TemplateSchema<TTemplate, TContext>);
    /**
     * Run all factory functions in the template and return final result to be
     * stored in the database.
     */
    resolve(overrides?: TemplateOverrides<TTemplate, TContext>): Promise<TemplateResult<TTemplate>>;
    extend<TOverrides>(overrides: ExtendSchema<TTemplate, TOverrides>): ExtendTemplate<TTemplate, TOverrides>;
}
/**
 * Factory Template definition.
 * Defines the fields that will be generated when the factory is called.
 */
type TemplateSchema<TTemplate, TContext = TemplateContext<TTemplate>> = {
    [key in keyof TTemplate]: TemplateField<TTemplate[key], TContext>;
} & ThisType<TContext>;
/**
 * Factory Template field.
 * Specifies a function to run every time the factory is called. Or a static
 * value that will always remain the same.
 */
type TemplateField<TValue = unknown, TContext = unknown> = TValue | Promise<TValue> | ((context: TContext) => TValue | Promise<TValue>);
/**
 * Factory Template result.
 * The raw output of type of the template after resolving all fields.
 */
type TemplateResult<TTemplate> = {
    [key in keyof TTemplate]: InferFieldType<TTemplate[key]>;
};
/**
 * Infer the resulting data type of the provided factory template.
 * @example
 * const userTemplate = new FactoryTemplate({
 *     userId: () => 123,
 *     createdAt: () => new Date()
 * })
 *
 * const data: InferOutput<typeof userTemplate>
 *     // -> { userId: number, createdAt: Date }
 */
type InferOutput<TTemplate> = TTemplate extends FactoryTemplate<infer T> ? T : never;
/**
 * Template overrides.
 * Defines the fields that can be overridden before resolving the final
 * template.
 */
type TemplateOverrides<TTemplate, TContext = {}> = {
    [key in keyof TTemplate]?: TemplateField<InferFieldType<TTemplate[key]>, TContext>;
} & ThisType<TContext>;
/**
 * Infer the resolved output type of a given template field.
 */
type InferFieldType<T> = T extends TemplateField<infer T> ? T : never;
/**
 * Merge two template definitions to create a new template using one as a base.
 */
type ExtendTemplate<TTemplate, TOverrides, TResult = TTemplate & TOverrides, TMerged = {
    [key in keyof TResult]: key extends keyof TOverrides ? TOverrides[key] : TResult[key];
}> = FactoryTemplate<TMerged>;
/**
 * Combine a target schema with a baseline schema.
 */
type ExtendSchema<TTemplate, TOverrides, TResult = TTemplate & TOverrides, TMerged = {
    [key in keyof TResult]: key extends keyof TOverrides ? TOverrides[key] : TResult[key];
}> = TOverrides & ThisType<TemplateContext<TMerged>>;

/**
 * Utility type for any Feathers or class that implements
 * an interface compatible with Feathers Factory.
 * @param TSchema The expected input type for the service create() method.
 * @param TResult The return type for the provided service class
 */
type FactoryService<TSchema = unknown, TResult = TSchema, TParams = Params> = FactoryCompatibleService<TSchema, TResult, TParams>;
interface FactoryCompatibleService<TSchema, TResult = TSchema, TParams = Params, TData = TSchema | TSchema[], TReturn = TResult | TResult[]> {
    create(data: TData, params?: TParams): TReturn | Promise<TReturn>;
}

declare class Factory<TSchema, TResult = TSchema, TParams = Params> {
    private readonly service;
    protected readonly data: FactoryTemplate<TSchema>;
    protected readonly params: FactoryTemplate<Params>;
    /**
     * Factory constructor.
     */
    constructor(service: FactoryCompatibleService<TSchema, TResult, TParams>, data: TemplateSchema<TSchema> | FactoryTemplate<TSchema>, defaultParams?: TemplateSchema<TParams>);
    /**
     * Store generated data to the Feathers service.
     */
    create(data?: TemplateOverrides<TSchema>, params?: TemplateOverrides<TParams>): Promise<TResult>;
    /**
     * Quickly populate the database running the factory a number of times.
     */
    createMany(quantity: number, overrides?: TemplateOverrides<TSchema>, params?: TemplateOverrides<TParams>): Promise<TResult[]>;
    /**
     * Just resolve a predefined factory template without inserting it into
     * the underlying service.
     *
     * @param overrides
     */
    get(overrides?: TemplateOverrides<TSchema>): Promise<TemplateResult<TSchema>>;
}

type TemplateTypes_ExtendSchema<TTemplate, TOverrides, TResult = TTemplate & TOverrides, TMerged = {
    [key in keyof TResult]: key extends keyof TOverrides ? TOverrides[key] : TResult[key];
}> = ExtendSchema<TTemplate, TOverrides, TResult, TMerged>;
type TemplateTypes_InferFieldType<T> = InferFieldType<T>;
type TemplateTypes_InferOutput<TTemplate> = InferOutput<TTemplate>;
declare namespace TemplateTypes {
  export type { ExtendTemplate as Extend, TemplateTypes_ExtendSchema as ExtendSchema, TemplateTypes_InferFieldType as InferFieldType, TemplateTypes_InferOutput as InferOutput, TemplateOverrides as Overrides, TemplateResult as Result, TemplateSchema as Schema };
}

type HasBeenAugmented<T> = [keyof T] extends [never] ? false : true;

/**
 * We are intentionally leaving this empty here so you can add type
 * declarations for globally accessible factories.
 *
 * @example
 * const MyUserFactory = new Factory(...);
 *
 * declare module 'feathers-factory' {
 *     interface GlobalFactories {
 *          'user-factory': typeof MyUserFactory
 *     }
 * }
 */
interface GlobalFactories {
}
declare const _default: {
    /**
     * Defined factories.
     */
    factories: FactoryRegistry;
    /**
     * Define a new factory.
     */
    define<TSchema, TResult = TSchema>(factoryName: FactoryName, factory: Factory<TSchema, TResult, Params>): void;
    /**
     * Retrieve a factory name as defined in the define() method.
     *
     * @param name
     */
    getFactory<TName extends FactoryName>(name: TName): GlobalFactory<TName>;
    /**
     * Run factory, creating entry in Feathers service.
     *
     * @param factoryName
     * @param overrides
     * @param params
     */
    create<TName extends FactoryName>(factoryName: TName, overrides?: GlobalFactoryOverrides<TName> | undefined, params?: Params): Promise<any>;
    /**
     * Run a number of factories, creating entries in Feathers service.
     *
     * @param quantity
     * @param factoryName
     * @param overrides
     * @param params
     */
    createMany<TName extends FactoryName>(quantity: number, factoryName: TName, overrides?: TemplateOverrides<any>, params?: Params): Promise<any[]>;
    /**
     * Run factory without creating entry in Feathers service.
     * Returns resolved data object.
     *
     * @param factoryName
     * @param overrides
     */
    get<TName extends FactoryName>(factoryName: TName, overrides?: TemplateOverrides<TName> | undefined): Promise<TemplateResult<any>>;
};

type FactoryName = keyof FactoryRegistry;
type GlobalFactory<TName extends FactoryName> = FactoryRegistry[TName];
type GlobalFactoryOverrides<TName extends FactoryName> = GlobalFactory<TName> extends Factory<infer TSchema, infer TResult> ? TemplateOverrides<TSchema> : never;
type FactoryRegistry = HasBeenAugmented<GlobalFactories> extends true ? GlobalFactories : DefaultFactoryRegistry;
type DefaultFactoryRegistry = {
    [key: string]: Factory<any, any>;
};

export { Factory, type FactoryService, FactoryTemplate, _default as GlobalFactories, TemplateTypes as Template, TemplateContext };
