import type { JSONSchema4 } from "./types/json-schema";
import { cloneDeep, merge } from "./compat";
import { format } from "./formatter";
import { generate } from "./generator";
import { normalize } from "./normalizer";
import { optimize } from "./optimizer";
import { parse } from "./parser";
import { dereference } from "./resolver";
import { validate } from "./validator";
import { link } from "./linker";
import { validateOptions } from "./optionValidator";
import type { JSONSchema as LinkedJSONSchema } from "./types/JSONSchema";

export type {
  EnumJSONSchema,
  JSONSchema,
  NamedEnumJSONSchema,
  CustomTypeJSONSchema,
} from "./types/JSONSchema";

export interface FormatOptions {
  bracketSpacing?: boolean;
  printWidth?: number;
  semi?: boolean;
  singleQuote?: boolean;
  tabWidth?: number;
  trailingComma?: "none" | "es5" | "all";
  useTabs?: boolean;
}

export type RefOptions = Record<string, unknown>;

export interface Options {
  /**
   * Preserved for upstream API compatibility.
   *
   * Executor's vendored compiler only resolves same-document JSON Pointer
   * `$ref`s and never fetches or reads external references, so this option is
   * accepted but ignored.
   */
  $refOptions: RefOptions;
  /**
   * Default value for additionalProperties, when it is not explicitly set.
   */
  additionalProperties: boolean;
  /**
   * Disclaimer comment prepended to the top of each generated file.
   */
  bannerComment: string;
  /**
   * Custom function to provide a type name for a given schema
   */
  customName?: (
    schema: LinkedJSONSchema,
    keyNameFromDefinition: string | undefined,
  ) => string | undefined;
  /**
   * Preserved for upstream API compatibility. External refs are not resolved,
   * so this option is accepted but ignored.
   */
  cwd: string;
  /**
   * Declare external schemas referenced via `$ref`?
   */
  declareExternallyReferenced: boolean;
  /**
   * Prepend enums with [`const`](https://www.typescriptlang.org/docs/handbook/enums.html#computed-and-constant-members)?
   */
  enableConstEnums: boolean;
  /**
   * Create enums from JSON enums with eponymous keys
   */
  inferStringEnumKeysFromValues: boolean;
  /**
   * Format code? Preserved for upstream API compatibility.
   *
   * Executor's vendored compiler does not bundle a formatter, so this option is
   * accepted but does not change output.
   */
  format: boolean;
  /**
   * Ignore maxItems and minItems for `array` types, preventing tuples being generated.
   */
  ignoreMinAndMaxItems: boolean;
  /**
   * Maximum number of unioned tuples to emit when representing bounded-size array types,
   * before falling back to emitting unbounded arrays. Increase this to improve precision
   * of emitted types, decrease it to improve performance, or set it to `-1` to ignore
   * `minItems` and `maxItems`.
   */
  maxItems: number;
  /**
   * Append all index signatures with `| undefined` so that they are strictly typed.
   *
   * This is required to be compatible with `strictNullChecks`.
   */
  strictIndexSignatures: boolean;
  /**
   * Formatting configuration. Preserved for upstream API compatibility.
   */
  style: FormatOptions;
  /**
   * Generate code for `definitions` that aren't referenced by the schema?
   */
  unreachableDefinitions: boolean;
  /**
   * Generate unknown type instead of any
   */
  unknownAny: boolean;
}

export const DEFAULT_OPTIONS: Options = {
  $refOptions: {},
  additionalProperties: true, // TODO: default to empty schema (as per spec) instead
  bannerComment: `/* eslint-disable */
/**
* This file was automatically generated by json-schema-to-typescript.
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
* and run json-schema-to-typescript to regenerate this file.
*/`,
  cwd: "",
  declareExternallyReferenced: true,
  enableConstEnums: true,
  inferStringEnumKeysFromValues: false,
  format: false,
  ignoreMinAndMaxItems: false,
  maxItems: 20,
  strictIndexSignatures: false,
  style: {
    bracketSpacing: false,
    printWidth: 120,
    semi: true,
    singleQuote: false,
    tabWidth: 2,
    trailingComma: "none",
    useTabs: false,
  },
  unreachableDefinitions: false,
  unknownAny: true,
};

export function compile(schema: JSONSchema4, name: string, options: Partial<Options> = {}): string {
  validateOptions(options);

  const _options = merge({} as Options, DEFAULT_OPTIONS, options);

  // Initial clone to avoid mutating the input
  const _schema = cloneDeep(schema);

  const { dereferencedPaths, dereferencedSchema } = dereference(_schema);
  const linked = link(dereferencedSchema);
  const errors = validate(linked, name);
  if (errors.length) {
    throw new ValidationError(errors.join("\n"));
  }

  const normalized = normalize(linked, dereferencedPaths, name, _options);
  const parsed = parse(normalized, _options);
  const optimized = optimize(parsed, _options);
  const generated = generate(optimized, _options);
  const formatted = format(generated, _options);

  return formatted;
}

export class ValidationError extends Error {}
