import EmberArray from '@ember/array';
import ArrayProxy from '@ember/array/proxy';
import EmberDataModel, { AsyncBelongsTo, AsyncHasMany } from '@ember-data/model';

import { BelongsTo, HasMany, ModelDefinition } from 'ember-cli-mirage/-types';

import { FilterKeysByType } from 'app/types/util';

/**
 * Converts the given Ember Data Model Registry into a Mirage Model Registry.
 *
 * @example
 * Given the following Registry:
 * ```ts
 * import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
 *
 * export default class User extends Model {
 *   @hasMany('post')
 *   declare posts: AsyncHasMany<Post>;
 * }
 *
 * export default class User extends Model {
 *   @belongsTo('user')
 *   declare user: AsyncBelongsTo<User>;
 * }
 *
 * export default interface ModelRegistry {
 *   user: User;
 *   post: Post;
 * }
 * ```
 *
 * We can generate the Mirage model Registry with:
 * ```ts
 * declare const model: ConvertModelRegistry<ModelRegistry>;
 * ```
 *
 * The resulting Mirage model Registry will look like:
 * ```
 * type Registry = {
 *   user: ModelDefinition<{ posts: HasMany<'post'> }>;
 *   post: ModelDefinition<{ posts: BelongsTo<'user'> }>;
 * }
 * ```
 *
 * Note that only relationships are included on the Mirage model data type.
 * We will get types from `@attr`s from Factories in the Mirage Registry,
 * and getters should not be included in Mirage types bc they don't exist on the
 * backend.
 */
export type ConvertModelRegistry<EmberDataModelRegistry> = {
  [K in keyof EmberDataModelRegistry]: ModelDefinitionFor<
    EmberDataModelRegistry,
    K
  >;
};

/**
 * Wraps the `ModelDefinitionData` for the given Ember Data Model Registry and
 * model key in Mirage's `ModelDefinition` type, which is necessary for the
 * Mirage Registry to recognize the data as a Mirage model.
 */
type ModelDefinitionFor<
  EmberDataModelRegistry,
  K extends keyof EmberDataModelRegistry
> = ModelDefinition<
  ModelDefinitionData<EmberDataModelRegistry, K> & { id: string }
>;

/**
 * Converts an Ember Data model into Data for a Mirage model definition.
 *
 * @example
 * Given the following Ember Data model for a user:
 * ```ts
 * import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
 *
 * export default class User extends Model {
 *   @belongsTo('organization')
 *   declare organization: PromiseRecord<Organization>;
 *
 *   @hasMany('user')
 *   declare friends: PromiseManyArray<User>;
 *
 *   @attr('string')
 *   declare name: string;
 *
 *   @attr('number')
 *   age?: number;
 *
 *   get description(): string {
 *     return `User ${name} is ${age} years old.`
 *   }
 * }
 *
 * export default interface ModelRegistry {
 *   user: User;
 * }
 * ```
 *
 * We can generate the Mirage model data for the User model with:
 * ```ts
 * declare const model: ModelDefinitionData<ModelRegistry, 'user'>;
 * ```
 *
 * The resulting Mirage model data type will look like:
 * ```
 * import { BelongsTo, HasMany } from 'miragejs/-types';
 *
 * type UserMirageModel = {
 *   organization: BelongsTo<'organization'>;
 *   friends: HasMany<'friend'>;
 * }
 * ```
 *
 * Note that the `@attr` properties and the `description` getter from the Ember
 * Data model are not included in the Mirage model data type. We will get types
 * from `@attr`s from Factories in the Mirage Registry, and getters should not
 * be included in Mirage types bc they don't exist on the backend.
 */
export type ModelDefinitionData<
  EmberDataModelRegistry,
  K extends keyof EmberDataModelRegistry
> = EmberDataModelRegistry[K] extends EmberDataModel
  ? MapEmberDataRelationshipsToMirage<
      EmberDataModelRegistry,
      RelationshipsFor<EmberDataModelRegistry[K]>
    >
  : never;

/**
 * Extract model attributes from the Ember Data model.
 */
type RelationshipsFor<M extends EmberDataModel> = Pick<
  M,
  FilterKeysByType<
    M,
    PromiseManyArray<EmberDataModel> | PromiseRecord<EmberDataModel>
  >
>;

/**
 * Maps Ember Data model types to Mirage model types.
 * Ember Data relationships will be converted to Mirage relationships.
 * All other attribute types will remain unchanged.
 */
type MapEmberDataRelationshipsToMirage<
  EmberDataModelRegistry,
  M extends Partial<EmberDataModel>
> = {
  [K in keyof M]: MapEmberDataRelationshipToMirage<
    EmberDataModelRegistry,
    M[K]
  >;
};

/**
 * Converts an Ember Data model attribute's type to a Mirage model type.
 * If T is a AsyncHasMany<M>,
 *  then it will be converted to HasMany<'m'>.
 * If T is a AsyncBelongsTo<M>,
 *  then it will be converted to BelongsTo<'m'>.
 * Otherwise, T will be never.
 */
type MapEmberDataRelationshipToMirage<
  EmberDataModelRegistry,
  T
> = T extends AsyncHasMany<infer Model>
  ? HasMany<ModelName<EmberDataModelRegistry, Model>>
  : T extends AsyncBelongsTo<infer Model>
  ? BelongsTo<ModelName<EmberDataModelRegistry, Model>>
  : never;

/**
 * Gets the ModelName from the Ember Data Model Registry for the given model.
 */
type ModelName<EmberDataModelRegistry, Model> = FilterKeysByType<
  EmberDataModelRegistry,
  Model
> extends string
  ? FilterKeysByType<EmberDataModelRegistry, Model>
  : never;