import type {
  MustConform,
  AnyObject,
  EmptyObject,
  DeepPartial,
  IsObject,
  Merge,
  RemoveIndexSignatures,
  SimpleMerge,
  Identity,
} from "#utils";
import type { SchemaField } from "../data/fields.d.mts";
import type { DatabaseCreateOperation, DatabaseDeleteOperation, DatabaseUpdateOperation } from "./_types.d.mts";
import type { DataModel } from "./data.d.mts";
import type Document from "./document.d.mts";

type DataSchema = foundry.data.fields.DataSchema;

interface _InternalTypeDataModelInterface extends DataModel.AnyConstructor {
  new <Schema extends DataSchema, Parent extends Document.Any, _ComputedInstance extends object>(
    ...args: DataModel.ConstructorArgs<Schema, Parent>

    // Note(LukeAbby): This seemingly redundant `DataModel<Schema, Parent>` is to
    // Ensure that TypeScript allows overriding `protected` methods in subclasses.
    // See: https://gist.github.com/LukeAbby/b9fd57eeba778a25297721e88b3e6bdd
  ): DataModel<Schema, Parent> & _ComputedInstance;
}

declare const _InternalTypeDataModelConst: _InternalTypeDataModelInterface;

// @ts-expect-error Ignore the error, this is a workaround for a dynamic class.
declare class _InternalTypeDataModel<
  Schema extends DataSchema,
  Parent extends Document<Document.Type, DataSchema, Document.Any | null>,
  BaseData extends AnyObject = EmptyObject,
  DerivedData extends AnyObject = EmptyObject,
  // This does not work if inlined. It's weird to put it here but it works.
  _ComputedInstance extends object = Merge<RemoveIndexSignatures<BaseData>, RemoveIndexSignatures<DerivedData>>,
> extends _InternalTypeDataModelConst<Schema, Parent, _ComputedInstance> {}

declare const __TypeDataModelBrand: unique symbol;

// These properties are used to give a performant way of inferring types like `BaseData` and `DerivedData` types.
// This avoids checking the entire constraint `T extends TypeDataModel.Any` to infer out the types.
// To prevent adding properties that could appear at runtime they are unique symbols.
// This makes them uniterable and means that the only way to access them is to have a reference to the variables which will be impossible outside of this file.
declare const __Schema: unique symbol;
declare const __Parent: unique symbol;
declare const __BaseModel: unique symbol;
declare const __BaseData: unique symbol;
declare const __DerivedData: unique symbol;

type _ClassMustBeAssignableToInternal = MustConform<typeof Document, Document.Internal.Constructor>;
type _InstanceMustBeAssignableToInternal = MustConform<Document.Any, Document.Internal.Instance.Any>;

// Removes the base and derived data from the type.
// Has no extends bounds to simplify any checking logic.
type RemoveDerived<BaseThis, BaseModel, BaseData, DerivedData> = SimpleMerge<
  Omit<BaseThis, keyof BaseData | keyof DerivedData>,
  BaseModel
>;

// Merges U into T but makes the appropriate keys partial.
// This is similar to `Merge<T, DeepPartial<U>>` but only makes the deepest keys in `U` optional.
type MergePartial<T, U> = Omit<T, keyof U> & {
  [K in keyof U as K extends PartialMergeKeys<T, U> ? K : never]?: InnerMerge<U, K, T>;
} & {
  [K in keyof U as K extends PartialMergeKeys<T, U> ? never : K]: InnerMerge<U, K, T>;
};

// Note(LukeAbby): The addition of `PropertyKey` seems to help simplify tsc's type checking.
// This will likely not be necessary after
type RequiredKeys<T> = PropertyKey &
  {
    [K in keyof T]-?: T extends { readonly [_ in K]: any } ? K : never;
  }[keyof T];

// Returns all the keys of U that should be partial when merged into T.
// Only if both `T[K]` and `U[K]` are required and both are objects should a key be required.
// This is because essentially only the most deep keys that are merged in need to be optional.
//
// Note(LukeAbby): The addition of `PropertyKey` seems to help simplify tsc's type checking.
type PartialMergeKeys<T, U> = PropertyKey &
  {
    [K in keyof U]-?: K extends RequiredKeys<U>
      ? K extends RequiredKeys<T>
        ? IsObject<T[K]> extends true
          ? IsObject<U[K]> extends true
            ? never
            : K
          : K
        : K
      : K;
  }[keyof U];

// Merges `U[K]` into `T[K]` if they're both objects, returns `U[K]` otherwise.
type InnerMerge<U, K extends keyof U, T> = T extends { readonly [_ in K]?: infer V }
  ? IsObject<U[K]> extends true
    ? IsObject<V> extends true
      ? MergePartial<V, U[K]>
      : Partial<U[K]>
    : Partial<U[K]>
  : U[K];

declare namespace TypeDataModel {
  interface Any extends AnyTypeDataModel {}
  interface AnyConstructor extends Identity<typeof AnyTypeDataModel> {}

  type ConfigurationFailureInstance = ConfigurationFailure;
  type ConfigurationFailureClass = typeof ConfigurationFailure;

  // Documented at https://gist.github.com/LukeAbby/c7420b053d881db4a4d4496b95995c98
  namespace Internal {
    type Constructor = (abstract new (...args: never) => Instance.Any) & {
      [__TypeDataModelBrand]: never;
    };

    // This still is only allows instances descended from `TypeDataField` because these unique symbols aren't used elsewhere.
    // These generic parameters seem to be required. This is likely because of a TypeScript soundness holes in which concrete types like `any` or `unknown`
    // will get treated bivariantly whereas type parameters get treated more safely.
    interface Instance<
      out Schema extends DataSchema,
      out Parent extends Document.Any,
      out BaseModel,
      out BaseData,
      out DerivedData,
    > {
      [__Schema]: Schema;
      [__Parent]: Parent;
      [__BaseModel]: BaseModel;
      [__BaseData]: BaseData;
      [__DerivedData]: DerivedData;
    }

    namespace Instance {
      interface Any extends Instance<any, any, any, any, any> {}
    }
  }

  type PrepareBaseDataThis<BaseThis extends Internal.Instance.Any> =
    BaseThis extends Internal.Instance<infer _1, infer _2, infer BaseModel, infer BaseData, infer DerivedData>
      ? MergePartial<Omit<RemoveDerived<BaseThis, BaseModel, BaseData, DerivedData>, "prepareBaseData">, BaseData>
      : never;

  type PrepareDerivedDataThis<BaseThis extends Internal.Instance.Any> =
    BaseThis extends Internal.Instance<infer _1, infer _2, infer BaseModel, infer BaseData, infer DerivedData>
      ? MergePartial<
          SimpleMerge<Omit<RemoveDerived<BaseThis, BaseModel, BaseData, DerivedData>, "prepareDerivedData">, BaseData>,
          DerivedData
        >
      : never;

  type ParentAssignmentType<Schema extends DataSchema, Parent extends Document.Internal.Instance.Any> = SimpleMerge<
    SchemaField.InitializedData<Document.SchemaFor<Parent>>,
    {
      // FIXME(LukeAbby): Callers handle making this partial when obvious.
      // However also should make system partial using the regular rules: if `initial` is assignable to the field or if `required` is false etc.
      system: SchemaField.InitializedData<Schema>;
    }
  >;
}

declare abstract class AnyTypeDataModel extends TypeDataModel<any, any, any, any> {
  constructor(...args: never);
}

/**
 * A specialized subclass of DataModel, intended to represent a Document's type-specific data.
 * Systems or Modules that provide DataModel implementations for sub-types of Documents (such as Actors or Items)
 * should subclass this class instead of the base DataModel class.
 *
 *
 * @example Registering a custom sub-type for a Module.
 *
 * **module.json**
 * ```json
 * {
 *   "id": "my-module",
 *   "esmodules": ["main.mjs"],
 *   "documentTypes": {
 *     "Actor": {
 *       "sidekick": {},
 *       "villain": {}
 *     },
 *     "JournalEntryPage": {
 *       "dossier": {},
 *       "quest": {
 *         "htmlFields": ["description"]
 *       }
 *     }
 *   }
 * }
 * ```
 *
 * **main.mjs**
 * ```js
 * Hooks.on("init", () => {
 *   Object.assign(CONFIG.Actor.dataModels, {
 *     "my-module.sidekick": SidekickModel,
 *     "my-module.villain": VillainModel
 *   });
 *   Object.assign(CONFIG.JournalEntryPage.dataModels, {
 *     "my-module.dossier": DossierModel,
 *     "my-module.quest": QuestModel
 *   });
 * });
 *
 * class QuestModel extends foundry.abstract.TypeDataModel {
 *   static defineSchema() {
 *     const fields = foundry.data.fields;
 *     return {
 *       description: new fields.HTMLField({required: false, blank: true, initial: ""}),
 *       steps: new fields.ArrayField(new fields.StringField())
 *     };
 *   }
 *
 *   prepareDerivedData() {
 *     this.totalSteps = this.steps.length;
 *   }
 * }
 * ```
 */
declare abstract class TypeDataModel<
  Schema extends DataSchema,
  Parent extends Document.Any,
  BaseData extends AnyObject = EmptyObject,
  DerivedData extends AnyObject = EmptyObject,
> extends _InternalTypeDataModel<Schema, Parent, BaseData, DerivedData> {
  static [__TypeDataModelBrand]: never;

  [__Schema]: Schema;
  [__Parent]: Parent;
  [__BaseModel]: DataModel<Schema, Parent>;
  [__BaseData]: RemoveIndexSignatures<BaseData>;
  [__DerivedData]: RemoveIndexSignatures<DerivedData>;

  modelProvider: System | Module | null;

  /**
   * A set of localization prefix paths which are used by this data model.
   */
  static LOCALIZATION_PREFIXES: string[];

  /**
   * Prepare data related to this DataModel itself, before any derived data is computed.
   *
   * Called before {@link ClientDocument.prepareBaseData | `ClientDocument#prepareBaseData`} in {@link ClientDocument.prepareData | `ClientDocument#prepareData`}.
   */
  prepareBaseData(this: TypeDataModel.PrepareBaseDataThis<this>): void;

  /**
   * Apply transformations of derivations to the values of the source data object.
   * Compute data fields whose values are not stored to the database.
   *
   * Called before {@link ClientDocument.prepareDerivedData | `ClientDocument#prepareDerivedData`} in {@link ClientDocument.prepareData | `ClientDocument#prepareData`}.
   */
  prepareDerivedData(this: TypeDataModel.PrepareDerivedDataThis<this>): void;

  /**
   * Convert this Document to some HTML display for embedding purposes.
   * @param config  - Configuration for embedding behavior.
   * @param options - The original enrichment options for cases where the Document embed content
   *                  also contains text that must be enriched.
   */
  toEmbed(
    config: TextEditor.DocumentHTMLEmbedConfig,
    options: TextEditor.EnrichmentOptions,
  ): Promise<HTMLElement | HTMLCollection | null>;

  /* -------------------------------------------- */
  /*  Database Operations                         */
  /* -------------------------------------------- */

  /**
   * Called by {@link ClientDocument._preCreate | `ClientDocument#_preCreate`}.
   *
   * @param data    - The initial data object provided to the document creation request
   * @param options - Additional options which modify the creation request
   * @param user    - The User requesting the document creation
   * @returns Return false to exclude this Document from the creation operation
   */
  protected _preCreate(
    data: TypeDataModel.ParentAssignmentType<Schema, Parent>,
    options: Document.Database.PreCreateOptions<DatabaseCreateOperation>,
    user: User.Implementation,
  ): Promise<boolean | void>;

  /**
   * Called by {@link ClientDocument._onCreate | `ClientDocument#_onCreate`}.
   *
   * @param data    - The initial data object provided to the document creation request
   * @param options - Additional options which modify the creation request
   * @param userId  - The id of the User requesting the document update
   */
  // TODO: should be `MaybePromise<void>` to allow async subclassing?
  protected _onCreate(
    data: TypeDataModel.ParentAssignmentType<Schema, Parent>,
    options: Document.Database.CreateOptions<DatabaseCreateOperation>,
    userId: string,
  ): void;

  /**
   * Called by {@link ClientDocument._preUpdate | `ClientDocument#_preUpdate`}.
   *
   * @param changes - The candidate changes to the Document
   * @param options - Additional options which modify the update request
   * @param user    - The User requesting the document update
   * @returns A return value of false indicates the update operation should be cancelled.
   */
  protected _preUpdate(
    changes: DeepPartial<TypeDataModel.ParentAssignmentType<Schema, Parent>>,
    options: Document.Database.PreUpdateOptions<DatabaseUpdateOperation>,
    user: User.Implementation,
  ): Promise<boolean | void>;

  /**
   * Called by {@link ClientDocument._onUpdate | `ClientDocument#_onUpdate`}.
   *
   * @param changed - The differential data that was changed relative to the documents prior values
   * @param options - Additional options which modify the update request
   * @param userId  - The id of the User requesting the document update
   */
  // TODO: should be `MaybePromise<void>` to allow async subclassing?
  protected _onUpdate(
    changed: DeepPartial<TypeDataModel.ParentAssignmentType<Schema, Parent>>,
    options: Document.Database.UpdateOptions<DatabaseUpdateOperation>,
    userId: string,
  ): void;

  /**
   * Called by {@link ClientDocument._preDelete | `ClientDocument#_preDelete`}.
   *
   * @param options - Additional options which modify the deletion request
   * @param user    - The User requesting the document deletion
   * @returns A return value of false indicates the deletion operation should be cancelled.
   */
  protected _preDelete(
    options: Document.Database.PreDeleteOperationInstance<DatabaseDeleteOperation>,
    user: User.Implementation,
  ): Promise<boolean | void>;

  /**
   * Called by {@link ClientDocument._onDelete | `ClientDocument#_onDelete`}.
   *
   * @param options - Additional options which modify the deletion request
   * @param userId  - The id of the User requesting the document update
   */
  // TODO: should be `MaybePromise<void>` to allow async subclassing?
  protected _onDelete(options: Document.Database.DeleteOptions<DatabaseDeleteOperation>, userId: string): void;
}

declare class ConfigurationFailure extends TypeDataModel<any, any, any, any> {}

export default TypeDataModel;
