import type { EntityConstructor, EntityInterface } from './Entity.types.js';
import type Nymph from './Nymph.js';

export type NymphEventType =
  | 'connect'
  | 'disconnect'
  | 'query'
  | 'beforeGetEntity'
  | 'beforeGetEntities'
  | 'beforeSaveEntity'
  | 'afterSaveEntity'
  | 'failedSaveEntity'
  | 'beforeDeleteEntity'
  | 'afterDeleteEntity'
  | 'failedDeleteEntity'
  | 'beforeDeleteEntityByID'
  | 'afterDeleteEntityByID'
  | 'failedDeleteEntityByID'
  | 'beforeNewUID'
  | 'afterNewUID'
  | 'failedNewUID'
  | 'beforeSetUID'
  | 'afterSetUID'
  | 'failedSetUID'
  | 'beforeRenameUID'
  | 'afterRenameUID'
  | 'failedRenameUID'
  | 'beforeDeleteUID'
  | 'afterDeleteUID'
  | 'failedDeleteUID'
  | 'beforeStartTransaction'
  | 'afterStartTransaction'
  | 'beforeCommitTransaction'
  | 'afterCommitTransaction'
  | 'beforeRollbackTransaction'
  | 'afterRollbackTransaction';
export type NymphConnectCallback = (
  nymph: Nymph,
  result: Promise<boolean>,
) => Promise<void>;
export type NymphDisconnectCallback = (
  nymph: Nymph,
  result: Promise<boolean>,
) => Promise<void>;
/**
 * The NymphQueryCallback will be called on both top level and qref queries.
 *
 * This is the only callback that is not asynchronous.
 *
 * This also isn't necessarily run on every "query". It is run before a database
 * query for an entity, but it is not run during PubSub entity change
 * propagation. Therefore, it shouldn't do anything _entity_ specific, instead
 * it should be _query_ specific. For example, throw an error if the user is not
 * permitted to run the query at all.
 */
export type NymphQueryCallback = (
  nymph: Nymph,
  options: Options,
  selectors: FormattedSelector[],
) => void;
export type NymphBeforeGetEntityCallback = (
  nymph: Nymph,
  options: Options,
  selectors: Selector[],
) => Promise<void>;
export type NymphBeforeGetEntitiesCallback = (
  nymph: Nymph,
  options: Options,
  selectors: Selector[],
) => Promise<void>;
export type NymphBeforeSaveEntityCallback = (
  nymph: Nymph,
  entity: EntityInterface,
) => Promise<void>;
export type NymphAfterSaveEntityCallback = (
  nymph: Nymph,
  result: Promise<boolean>,
) => Promise<void>;
export type NymphFailedSaveEntityCallback = (
  nymph: Nymph,
  error: any,
) => Promise<void>;
export type NymphBeforeDeleteEntityCallback = (
  nymph: Nymph,
  entity: EntityInterface,
) => Promise<void>;
export type NymphAfterDeleteEntityCallback = (
  nymph: Nymph,
  result: Promise<boolean>,
) => Promise<void>;
export type NymphFailedDeleteEntityCallback = (
  nymph: Nymph,
  error: any,
) => Promise<void>;
export type NymphBeforeDeleteEntityByIDCallback = (
  nymph: Nymph,
  guid: string,
  className?: string,
) => Promise<void>;
export type NymphAfterDeleteEntityByIDCallback = (
  nymph: Nymph,
  result: Promise<boolean>,
) => Promise<void>;
export type NymphFailedDeleteEntityByIDCallback = (
  nymph: Nymph,
  error: any,
) => Promise<void>;
export type NymphBeforeNewUIDCallback = (
  nymph: Nymph,
  name: string,
) => Promise<void>;
export type NymphAfterNewUIDCallback = (
  nymph: Nymph,
  result: Promise<number | null>,
) => Promise<void>;
export type NymphFailedNewUIDCallback = (
  nymph: Nymph,
  error: any,
) => Promise<void>;
export type NymphBeforeSetUIDCallback = (
  nymph: Nymph,
  name: string,
  value: number,
) => Promise<void>;
export type NymphAfterSetUIDCallback = (
  nymph: Nymph,
  result: Promise<boolean>,
) => Promise<void>;
export type NymphFailedSetUIDCallback = (
  nymph: Nymph,
  error: any,
) => Promise<void>;
export type NymphBeforeRenameUIDCallback = (
  nymph: Nymph,
  oldName: string,
  newName: string,
) => Promise<void>;
export type NymphAfterRenameUIDCallback = (
  nymph: Nymph,
  result: Promise<boolean>,
) => Promise<void>;
export type NymphFailedRenameUIDCallback = (
  nymph: Nymph,
  error: any,
) => Promise<void>;
export type NymphBeforeDeleteUIDCallback = (
  nymph: Nymph,
  name: string,
) => Promise<void>;
export type NymphAfterDeleteUIDCallback = (
  nymph: Nymph,
  result: Promise<boolean>,
) => Promise<void>;
export type NymphFailedDeleteUIDCallback = (
  nymph: Nymph,
  error: any,
) => Promise<void>;
export type NymphBeforeStartTransactionCallback = (
  nymph: Nymph,
  name: string,
) => Promise<void>;
export type NymphAfterStartTransactionCallback = (
  nymph: Nymph,
  name: string,
  result: Nymph,
) => Promise<void>;
export type NymphBeforeCommitTransactionCallback = (
  nymph: Nymph,
  name: string,
) => Promise<void>;
export type NymphAfterCommitTransactionCallback = (
  nymph: Nymph,
  name: string,
  result: boolean,
) => Promise<void>;
export type NymphBeforeRollbackTransactionCallback = (
  nymph: Nymph,
  name: string,
) => Promise<void>;
export type NymphAfterRollbackTransactionCallback = (
  nymph: Nymph,
  name: string,
  result: boolean,
) => Promise<void>;

export type Options<T extends EntityConstructor = EntityConstructor> = {
  /**
   * The Entity class to query.
   */
  class?: T;
  /**
   * The limit of entities to be returned. Not needed when using `getEntity`, as
   * it always returns only one.
   */
  limit?: number;
  /**
   * The offset from the first matching entity, in order, to start retrieving.
   */
  offset?: number;
  /**
   * If true, entities will be retrieved from newest to oldest/largest to
   * smallest (with regard to `sort`).
   */
  reverse?: boolean;
  /**
   * How to sort the entities. Should be "cdate", "mdate", or the name of a
   * property.
   *
   * When null, the result set will not be sorted in any particular way. It's
   * really up to the DB how the results are returned. In this case, limit and
   * offset may work for pagination, but shouldn't be relied on.
   */
  sort?: 'cdate' | 'mdate' | string | null;
  /**
   * What to return, the entities with their data, just the entity data, just
   * the GUIDs, or just a count.
   *
   * 'object' is only available on the server, as there are substantial security
   * implications with using it. It returns the full entity data as a simple
   * object. You should only use it for operations where instantiating the
   * entities has an unacceptable performance cost.
   */
  return?: 'entity' | 'object' | 'guid' | 'count';
  /**
   * Will be 'client' if the query came from a REST request or the PubSub
   * server. (Mainly used in Tilmeld for access control.)
   */
  source?: string;
  /**
   * If true, Nymph will skip the cache and retrieve the entity from the DB.
   */
  skipCache?: boolean;
  /**
   * The level(s) of access control requested.
   *
   * Limiting the access control can significantly improve the performance of
   * the query.
   */
  acRequest?: TilmeldAccessRequest;
  /**
   * If true, Tilmeld will not filter returned entities according to access
   * controls. (If Tilmeld is installed.) (This is always set to false by the
   * REST endpoint and PubSub server.)
   */
  skipAc?: boolean;
};

export type Clause<C> = C | Exclude<C, undefined>[];

export type OrWithTime<T> = T | [string, null, string];

type PrimitiveSelector = {
  guid?: string;
  '!guid'?: PrimitiveSelector['guid'];

  tag?: string;
  '!tag'?: PrimitiveSelector['tag'];

  defined?: string;
  '!defined'?: PrimitiveSelector['defined'];

  truthy?: string;
  '!truthy'?: PrimitiveSelector['truthy'];

  equal?: [string, any];
  '!equal'?: PrimitiveSelector['equal'];

  contain?: [string, any];
  '!contain'?: PrimitiveSelector['contain'];

  search?: [string, string];
  '!search'?: PrimitiveSelector['search'];

  match?: [string, string];
  '!match'?: PrimitiveSelector['match'];

  imatch?: [string, string];
  '!imatch'?: PrimitiveSelector['imatch'];

  like?: [string, string];
  '!like'?: PrimitiveSelector['like'];

  ilike?: [string, string];
  '!ilike'?: PrimitiveSelector['ilike'];

  gt?: [string, number];
  '!gt'?: PrimitiveSelector['gt'];

  gte?: [string, number];
  '!gte'?: PrimitiveSelector['gte'];

  lt?: [string, number];
  '!lt'?: PrimitiveSelector['lt'];

  lte?: [string, number];
  '!lte'?: PrimitiveSelector['lte'];

  ref?: [string, EntityInterface | string];
  '!ref'?: PrimitiveSelector['ref'];

  qref?: [string, [Options, ...PrimitiveSelector[]]];
  '!qref'?: PrimitiveSelector['qref'];

  selector?: PrimitiveSelector;
  '!selector'?: PrimitiveSelector['selector'];
};

export type Selector = {
  type: '&' | '|' | '!&' | '!|';

  guid?: Clause<PrimitiveSelector['guid']>;
  '!guid'?: Clause<PrimitiveSelector['guid']>;

  tag?: Clause<PrimitiveSelector['tag']>;
  '!tag'?: Clause<PrimitiveSelector['tag']>;

  defined?: Clause<PrimitiveSelector['defined']>;
  '!defined'?: Clause<PrimitiveSelector['defined']>;

  truthy?: Clause<PrimitiveSelector['truthy']>;
  '!truthy'?: Clause<PrimitiveSelector['truthy']>;

  equal?: Clause<OrWithTime<PrimitiveSelector['equal']>>;
  '!equal'?: Clause<OrWithTime<PrimitiveSelector['equal']>>;

  contain?: Clause<OrWithTime<PrimitiveSelector['contain']>>;
  '!contain'?: Clause<OrWithTime<PrimitiveSelector['contain']>>;

  search?: Clause<PrimitiveSelector['search']>;
  '!search'?: Clause<PrimitiveSelector['search']>;

  match?: Clause<PrimitiveSelector['match']>;
  '!match'?: Clause<PrimitiveSelector['match']>;

  imatch?: Clause<PrimitiveSelector['imatch']>;
  '!imatch'?: Clause<PrimitiveSelector['imatch']>;

  like?: Clause<PrimitiveSelector['like']>;
  '!like'?: Clause<PrimitiveSelector['like']>;

  ilike?: Clause<PrimitiveSelector['ilike']>;
  '!ilike'?: Clause<PrimitiveSelector['ilike']>;

  gt?: Clause<OrWithTime<PrimitiveSelector['gt']>>;
  '!gt'?: Clause<OrWithTime<PrimitiveSelector['gt']>>;

  gte?: Clause<OrWithTime<PrimitiveSelector['gte']>>;
  '!gte'?: Clause<OrWithTime<PrimitiveSelector['gte']>>;

  lt?: Clause<OrWithTime<PrimitiveSelector['lt']>>;
  '!lt'?: Clause<OrWithTime<PrimitiveSelector['lt']>>;

  lte?: Clause<OrWithTime<PrimitiveSelector['lte']>>;
  '!lte'?: Clause<OrWithTime<PrimitiveSelector['lte']>>;

  ref?: Clause<PrimitiveSelector['ref']>;
  '!ref'?: Clause<PrimitiveSelector['ref']>;

  qref?: Clause<[string, [Options, ...Selector[]]]>;
  '!qref'?: Selector['qref'];

  selector?: Clause<Selector>;
  '!selector'?: Selector['selector'];
};

export type FormattedSelector = {
  type: '&' | '|' | '!&' | '!|';

  guid?: PrimitiveSelector['guid'][][];
  '!guid'?: PrimitiveSelector['guid'][][];

  tag?: PrimitiveSelector['tag'][][];
  '!tag'?: PrimitiveSelector['tag'][][];

  defined?: PrimitiveSelector['defined'][][];
  '!defined'?: PrimitiveSelector['defined'][][];

  truthy?: PrimitiveSelector['truthy'][][];
  '!truthy'?: PrimitiveSelector['truthy'][][];

  equal?: PrimitiveSelector['equal'][];
  '!equal'?: PrimitiveSelector['equal'][];

  contain?: PrimitiveSelector['contain'][];
  '!contain'?: PrimitiveSelector['contain'][];

  search?: PrimitiveSelector['search'][];
  '!search'?: PrimitiveSelector['search'][];

  match?: PrimitiveSelector['match'][];
  '!match'?: PrimitiveSelector['match'][];

  imatch?: PrimitiveSelector['imatch'][];
  '!imatch'?: PrimitiveSelector['imatch'][];

  like?: PrimitiveSelector['like'][];
  '!like'?: PrimitiveSelector['like'][];

  ilike?: PrimitiveSelector['ilike'][];
  '!ilike'?: PrimitiveSelector['ilike'][];

  gt?: PrimitiveSelector['gt'][];
  '!gt'?: PrimitiveSelector['gt'][];

  gte?: PrimitiveSelector['gte'][];
  '!gte'?: PrimitiveSelector['gte'][];

  lt?: PrimitiveSelector['lt'][];
  '!lt'?: PrimitiveSelector['lt'][];

  lte?: PrimitiveSelector['lte'][];
  '!lte'?: PrimitiveSelector['lte'][];

  ref?: PrimitiveSelector['ref'][];
  '!ref'?: PrimitiveSelector['ref'][];

  qref?: [string, [Options, ...FormattedSelector[]]][];
  '!qref'?: FormattedSelector['qref'];

  selector?: FormattedSelector[];
  '!selector'?: FormattedSelector['selector'];
};

export enum TilmeldAccessLevels {
  NO_ACCESS = 0,
  READ_ACCESS = 1,
  WRITE_ACCESS = 2,
  // Keeping 3 open in case we ever need one between write and full.
  FULL_ACCESS = 4,
}

export enum TilmeldAccessRequest {
  /**
   * The default level, any possible entity readable by the user.
   *
   * This is the same as or-ing together all of the levels listed below.
   */
  ALL_LEVELS = 0,
  /**
   * Owned by and accessible to the user.
   *
   * This is always enabled, so not including it has no effect. It's here to
   * allow the selection of _only_ user owned entities.
   */
  USER_OWNED = 1,
  /**
   * Owned by and accessible to the user's primary group.
   */
  PRIMARY_GROUP_OWNED = 2,
  /**
   * Owned by and accessible to one of the user's secondary groups.
   */
  SECONDARY_GROUP_OWNED = 4,
  /**
   * The user is listed in an access control list.
   */
  USER_ACCESSIBLE = 8,
  /**
   * The user's primary group is listed in an access control list.
   */
  PRIMARY_GROUP_ACCESSIBLE = 16,
  /**
   * One of the user's secondary groups is listed in an access control list.
   */
  SECONDARY_GROUP_ACCESSIBLE = 32,
  /**
   * The entity is accessible to any user.
   */
  OTHER_ACCESSIBLE = 64,
  /**
   * The entity is not owned by anyone.
   */
  UNOWNED = 128,
}

export interface TilmeldInterface {
  nymph: Nymph;
  request: any;
  response: any;
  init(nymph: Nymph): void;
  clone(): TilmeldInterface;
  gatekeeper(ability?: string): boolean;
  authenticate(skipXsrfToken?: boolean, skipRenew?: boolean): Promise<boolean>;
  clearSession(): void;
  extractToken(token: string): Promise<any>;
  fillSession(user: any): Promise<void>;
  checkClientUIDPermissions(
    name: string,
    type?: TilmeldAccessLevels,
  ): Promise<boolean>;
}
