import { Logger } from "@lbu/insight";
import { AxiosInstance } from "axios";

/**
 * Check if value may be output object from a TypeBuilder
 */
export function isNamedTypeBuilderLike(value: any): boolean;

/**
 * Load a LBU structure from an LBU enabled API
 */
export function loadFromRemote(axios: AxiosInstance, baseUrl: string): any;

/**
 * Try to convert a OpenAPI spec object to LBU structure
 * @param defaultGroup Default to group to use for non tagged items in the spec
 * @param data Raw OpenAPI 3 json object
 */
export function loadFromOpenAPISpec(defaultGroup: string, data: any): any;

interface AppOpts {
  verbose?: boolean;
}

interface GenerateOpts {
  /**
   * Enabling specific groups so different generator combinations can be used.
   * The machinery will automatically find referenced types and include those
   * If this is undefined, all groups will be enabled
   */
  enabledGroups?: string[];

  /**
   * Used to conditional generate various things.
   * Is inferred when the reactQuery generator is enabled.
   *   isBrowser: true,
   *   isNodeServer: false,
   *   isNode: false,
   *   enabledGenerators: ["type", "validator", "apiClient", "reactQuery"],
   *   useTypescript: true,
   *   dumpStructure: false,
   *   dumpPostgres: false,
   */
  isBrowser?: boolean;

  /**
   * Used to conditional generate various things.
   * Is inferred when the sql or router generator is enabled.
   * Has a higher precedence than isNode, but also implies isNode.
   *   isBrowser: false,
   *   isNodeServer: true,
   *   isNode: true,
   *   enabledGenerators: ["type", "validator", "sql", "router", "apiClient"],
   *   useTypescript: false,
   *   dumpStructure: true,
   *   dumpPostgres: true,
   */
  isNodeServer?: boolean;

  /**
   * Used to conditional generate various things.
   * Is inferred when the react-query generator is disabled.
   * Has a lower precedence than isServer.
   *   isBrowser: false,
   *   isNodeServer: false,
   *   isNode: true,
   *   enabledGenerators: ["type", "validator"],
   *   useTypescript: false,
   *   dumpStructure: false,
   *   dumpPostgres: false,
   */
  isNode?: boolean;

  /**
   * Enabling specific generators.
   * If this is undefined, all registered generators are enabled
   */
  enabledGenerators?: string[];

  /**
   * Enable Typescript for the generators that support it
   */
  useTypescript?: boolean;

  /**
   * Dump a structure.js file with the used payload in it
   */
  dumpStructure?: boolean;

  /**
   * Custom file header to for example disable linting or something
   */
  fileHeader?: string;

  /**
   * Directory to write the files to
   */
  outputDirectory: string;
}

/**
 * The entry-point to code generation
 * Provides the structure for creating types, and extending with external sources.
 * Also maintains the generators
 */
export class App {
  /**
   * List used in the file header to ignore some eslint rules
   */
  static defaultEslintIgnore: string[];

  /**
   * Enable more logging while generating
   */
  public verbose: boolean;

  /**
   * Internally used logger
   */
  public logger: Logger;

  /**
   * Create a new App instance and inits generators
   */
  static new(options: AppOpts): Promise<App>;

  /**
   * Add new TypeBuilders to this app
   */
  add(...builders: TypeBuilder[]): App;

  /**
   * Add relations to the provided reference.
   * The provided reference must already exist.
   * This only works when referencing in to structure that you've passed in to
   * `app.extend`.
   */
  addRelations(reference: ReferenceType, ...relations: RelationType[]): App;

  /**
   * Add a raw object to this app.
   * Note that it throws when you are not conforming to at least the structure from the
   * TypeBuilder
   */
  addRaw(obj: any): App;

  /**
   * Add all groups and items to this App instance
   */
  extend(data: any): App;

  /**
   * Call the generators with the provided options
   * and writes the output
   */
  generate(options: GenerateOpts): Promise<void>;
}

export type TypeBuilderLike =
  | boolean
  | number
  | string
  | TypeBuilderLikeArray
  | TypeBuilderLikeObject
  | TypeBuilder;

interface TypeBuilderLikeArray extends Array<TypeBuilderLike> {}

interface TypeBuilderLikeObject extends Record<string, TypeBuilderLike> {}

/**
 * Create new instances of registered types and manages grups
 * Also keeps a Map of registered types on TypeCreator.types
 *
 * Note that all functions that return a `T extends TypeBuilder` are dynamically added and
 * provided by the core.
 */
export class TypeCreator {
  constructor(group?: string);

  /**
   * Represents any type
   */
  any(name?: string): AnyType;

  /**
   * Represent one of the provided types.
   * @param name
   */
  anyOf(name?: string): AnyOfType;

  /**
   * Represents a plain array
   */
  array(name?: string): ArrayType;

  /**
   * Boolean support
   */
  bool(name?: string): BooleanType;

  /**
   * Inputs ISO dates or js Date objects
   * @param name
   */
  date(name?: string): DateType;

  /**
   * Formidable file object, Blob or Stream depending on the usecase
   * @param name
   */
  file(name?: string): FileType;

  /**
   * A generic object, where both keys and values can be typed
   */
  generic(name?: string): GenericType;

  /**
   * Any number, integer by default but floating point can be enabled
   * @param name
   */
  number(name?: string): NumberType;

  /**
   * Plain old js object
   */
  object(name?: string): ObjectType;

  /**
   * Omit keys from the provided object type
   */
  omit(name?: string): OmitType;

  /**
   * Make a copy of the provided type, making it optional
   */
  optional(name?: string): OptionalType;

  /**
   * Pick keys from the provided object type
   */
  pick(name?: string): PickType;

  /**
   * Reference any other type, as if it was created inline
   */
  reference(groupOrOther?: string | TypeBuilder, name?: string): ReferenceType;

  /**
   * Make a copy of the provided type, making it sql searchable
   */
  searchable(name?: string): SearchableType;

  /**
   * Any string use case
   */
  string(name?: string): StringType;

  /**
   * Verified uuid
   */
  uuid(name?: string): UuidType;

  /**
   * Create a new RouteCreator
   * Provided by the 'router' generator
   */
  router(path: string): RouteCreator;

  /**
   * Create a oneToMany relation
   */
  oneToMany(ownKey: string, reference: ReferenceType): RelationType;

  /**
   * Create a manyOneMany relation
   */
  manyToOne(
    ownKey: string,
    reference: ReferenceType,
    referencedKey: string,
  ): RelationType;

  /**
   * Create a oneToOne relation
   */
  oneToOne(
    ownKey: string,
    reference: ReferenceType,
    referencedKey: string,
  ): RelationType;
}

/**
 * Provide base properties for types
 * This includes the 'type', optional, docs and default value.
 * Also contains group and name information
 */
export class TypeBuilder {
  static baseData: {
    type?: string;
    group?: string;
    name?: string;
    docString: string;
    isOptional: boolean;
    defaultValue?: string;
  };

  static getBaseData(): typeof TypeBuilder.baseData;

  /**
   * Create a new TypeBuilder for the provided group
   */
  constructor(type: string, group?: string, name?: string);

  public data: typeof TypeBuilder.baseData;

  /**
   * Add a doc comment, some generators / types may support rendering this
   */
  docs(docValue: string): this;

  /**
   * Value can be undefined
   */
  optional(): this;

  /**
   * Value may be null, only implemented for the primitives.
   */
  allowNull(): this;

  /**
   * Set a raw default value, also makes the type optional
   * Can be reverted by calling this function with undefined or null
   */
  default(rawString?: string | boolean | number): this;

  /**
   * Returns a shallow copy of the data object
   */
  build(): Record<string, any>;

  /**
   * Set this field as searchable for the 'sql' plugin
   */
  searchable(): this;

  /**
   * Set this field as primary for the 'sql' plugin
   */
  primary(): this;
}

/**
 * 'Router' plugin provided custom builder for api routes
 */
export class RouteBuilder extends TypeBuilder {
  /**
   * Add tags to this route.
   */
  tags(...value: string[]): this;

  /**
   * Type of accepted query parameters
   */
  query(builder: TypeBuilderLike): this;

  /**
   * Type of accepted path parameters
   */
  params(builder: TypeBuilderLike): this;

  /**
   * Type of accepted body parameters
   */
  body(builder: TypeBuilderLike): this;

  /**
   * Type of accepted file parameters
   */
  files(builder: TypeBuilderLike): this;

  /**
   * Route response type
   */
  response(builder: TypeBuilderLike): this;
}

export class RouteCreator {
  /**
   * Create a new route group.
   * Path will be concatenated with the current path of this group.
   * This resets the default tags, query, params, body and response type.
   */
  group(name: string, path: string): this;

  /**
   * GET route
   */
  get(path?: string, name?: string);

  /**
   * POST route
   */
  post(path?: string, name?: string);

  /**
   * PUT route
   */
  put(path?: string, name?: string);

  /**
   * PATCH route
   */
  patch(path?: string, name?: string);

  /**
   * DELETE route
   */
  delete(path?: string, name?: string);

  /**
   * HEAD route
   */
  head(path?: string, name?: string);

  /**
   * Default tags for all routes created by this RouteCreator.
   */
  tags(...value: string[]): this;

  /**
   * Default query type for all routes created by this RouteCreator.
   */
  query(builder: TypeBuilderLike): this;

  /**
   * Default params type for all routes created by this RouteCreator.
   */
  params(builder: TypeBuilderLike): this;

  /**
   * Default body type for all routes created by this RouteCreator.
   */
  body(builder: TypeBuilderLike): this;

  /**
   * Default files type for all routes created by this RouteCreator.
   */
  files(builder: TypeBuilderLike): this;

  /**
   * Default response type for all routes created by this RouteCreator.
   */
  response(builder: TypeBuilderLike): this;
}

export class AnyType extends TypeBuilder {
  raw(
    value: string,
    importValue?: { javaScript?: string; typeScript?: string },
  ): this;
}

export class AnyOfType extends TypeBuilder {
  values(...items: TypeBuilderLike[]): this;
}

export class ArrayType extends TypeBuilder {
  values(value: TypeBuilderLike): this;

  /**
   * Validator converts single item to an array
   */
  convert(): this;

  /**
   * Validator enforced minimum length inclusive
   */
  min(min: number): this;

  /**
   * Validator enforced maximum length inclusive
   */
  max(max: number): this;
}

export class BooleanType extends TypeBuilder {
  /**
   * Only accepts a specific value
   */
  oneOf(value: boolean): this;

  /**
   * Validator converts "true", "false", 0 and 1 to a boolean
   */
  convert(): this;
}

export class DateType extends TypeBuilder {
  defaultToNow(): this;
}

export class FileType extends TypeBuilder {}

export class GenericType extends TypeBuilder {
  keys(key: TypeBuilderLike): this;

  values(value: TypeBuilderLike): this;
}

export class NumberType extends TypeBuilder {
  /**
   * Only accepts a number from the provided set
   */
  oneOf(...value: number[]): this;

  /**
   * Try to convert a string to a number in the validator
   */
  convert(): this;

  /**
   * Validator does not enforce an integer
   */
  float(): this;

  /**
   * Validator enforced minimum value inclusive
   */
  min(min: number): this;

  /**
   * Validator enforced maximum value inclusive
   */
  max(max: number): this;
}

export class ObjectType extends TypeBuilder {
  keys(obj: Record<string, TypeBuilderLike>): this;

  /**
   * Validator allows extra keys on the incoming object
   */
  loose(): this;

  /**
   * Generate sql queries for this object
   * Possibly adding createdAt and updatedAt fields.
   * When withSoftDeletes is true, it automatically enables withDates.
   * Added by the 'sql' plugin
   */
  enableQueries(options?: {
    withSoftDeletes?: true;
    withDates?: true;
    withPrimaryKey?: false;
    isView?: true;
  }): this;

  /**
   * Add SQL relations
   */
  relations(...relations: RelationType[]): this;
}

export class OmitType extends TypeBuilder {
  /**
   * Set the object to operate on
   */
  object(builder: ObjectType | TypeBuilderLikeObject): this;

  /**
   * Keys to remove from the provided builder
   */
  keys(...keys: string[]): this;
}

export class OptionalType extends TypeBuilder {
  /**
   * Set the type to operate on
   */
  value(builder: TypeBuilderLike): this;
}

export class PickType extends TypeBuilder {
  /**
   * Set the object to operate on
   */
  object(builder: ObjectType | TypeBuilderLikeObject): this;

  /**
   * Keys to keep from the provided builder
   */
  keys(...keys: string[]): this;
}

export class ReferenceType extends TypeBuilder {}

/**
 * Relations are created by `T.oneToMany`, `T.manyToOne`, etc
 * The generator will warn you when relations are missing.
 */
export class RelationType {
  constructor(
    subType: string,
    ownKey: string,
    reference: ReferenceType,
    referencedKey?: string,
  );

  /**
   * Make this side of the relation optional
   */
  optional(): this;
}

export class SearchableType extends TypeBuilder {
  /**
   * Set the type to operate on
   */
  value(builder: TypeBuilderLike): this;
}

export class StringType extends TypeBuilder {
  /**
   * Only accepts a string from the provided set.
   * Also the way to make enums
   */
  oneOf(...values: string[]): this;

  /**
   * Validator tries to convert to string
   */
  convert(): this;

  /**
   * Validator trims the input
   */
  trim(): this;

  /**
   * Validator upper cases the input
   */
  upperCase(): this;

  /**
   * Validator lower cases the input
   */
  lowerCase(): this;

  /**
   * Validator enforced minimum length inclusive
   */
  min(min: number): this;

  /**
   * Validator enforced maximum length inclusive
   */
  max(max: number): this;

  /**
   * Validator enforced pattern
   */
  pattern(pattern: RegExp): this;
}

export class UuidType extends TypeBuilder {}

/**
 * Returns the list of groups that contain an item with the specified type.
 * Can be used to find all groups that expose part of the apiClient.
 */
export function getGroupsThatIncludeType(data: any, type: string): string[];
