import {
  assertString,
  assertFunction,
  assertObject,
  msg,
} from "./option-assertions.ts";

import type {
  ValidatorSet,
  Validator,
  OptionPath,
  RootPath,
} from "./option-assertions.ts";
import type { parse, ParserOptions } from "@babel/parser";
import type { Visitor } from "@babel/traverse";
import type { ResolvedOptions } from "./options.ts";
import type { File, PluginAPI, PluginPass } from "../../index.ts";
import type { GeneratorOptions, GeneratorResult } from "@babel/generator";
import type babelGenerator from "@babel/generator";

// Note: The casts here are just meant to be static assertions to make sure
// that the assertion functions actually assert that the value's type matches
// the declared types.
const VALIDATORS: ValidatorSet = {
  name: assertString as Validator<PluginObject["name"]>,
  manipulateOptions: assertFunction as Validator<
    PluginObject["manipulateOptions"]
  >,
  pre: assertFunction as Validator<PluginObject["pre"]>,
  post: assertFunction as Validator<PluginObject["post"]>,
  inherits: assertFunction as Validator<PluginObject["inherits"]>,
  visitor: assertVisitorMap as Validator<PluginObject["visitor"]>,

  parserOverride: assertFunction as Validator<PluginObject["parserOverride"]>,
  generatorOverride: assertFunction as Validator<
    PluginObject["generatorOverride"]
  >,
};

function assertVisitorMap(loc: OptionPath, value: unknown): Visitor {
  const obj = assertObject(loc, value);
  if (obj) {
    Object.keys(obj).forEach(prop => {
      if (prop !== "_exploded" && prop !== "_verified") {
        assertVisitorHandler(prop, obj[prop]);
      }
    });

    if (obj.enter || obj.exit) {
      throw new Error(
        `${msg(
          loc,
        )} cannot contain catch-all "enter" or "exit" handlers. Please target individual nodes.`,
      );
    }
  }
  return obj as Visitor;
}

function assertVisitorHandler(
  key: string,
  value: unknown,
): asserts value is VisitorHandler {
  if (value && typeof value === "object") {
    Object.keys(value).forEach((handler: string) => {
      if (handler !== "enter" && handler !== "exit") {
        throw new Error(
          `.visitor["${key}"] may only have .enter and/or .exit handlers.`,
        );
      }
    });
  } else if (typeof value !== "function") {
    throw new Error(`.visitor["${key}"] must be a function`);
  }
}

type VisitorHandler =
  | Function
  | {
      enter?: Function;
      exit?: Function;
    };

export type PluginObject<S extends PluginPass = PluginPass> = {
  name?: string;
  manipulateOptions?: (
    options: ResolvedOptions & { generatorOpts: GeneratorOptions },
    parserOpts: ParserOptions & {
      plugins: NonNullable<ParserOptions["plugins"]>;
    },
  ) => void;
  pre?: (this: S, file: File) => void | Promise<void>;
  post?: (this: S, file: File) => void | Promise<void>;
  inherits?: (api: PluginAPI, options: any, dirname: string) => PluginObject;
  visitor?: Visitor<S>;
  parserOverride?: (
    ...args: [...Parameters<typeof parse>, typeof parse]
  ) => ReturnType<typeof parse>;
  generatorOverride?: (
    ast: File["ast"],
    generatorOpts: GeneratorOptions,
    code: File["code"],
    generate: typeof babelGenerator,
  ) => GeneratorResult;
};

export function validatePluginObject(
  obj: Record<string, unknown>,
): PluginObject {
  const rootPath: RootPath = {
    type: "root",
    source: "plugin",
  };
  Object.keys(obj).forEach((key: string) => {
    const validator = VALIDATORS[key];

    if (validator) {
      const optLoc: OptionPath = {
        type: "option",
        name: key,
        parent: rootPath,
      };
      validator(optLoc, obj[key]);
    } else {
      const invalidPluginPropertyError = new Error(
        `.${key} is not a valid Plugin property`,
      );
      // @ts-expect-error todo(flow->ts) consider adding BabelConfigError with code field
      invalidPluginPropertyError.code = "BABEL_UNKNOWN_PLUGIN_PROPERTY";
      throw invalidPluginPropertyError;
    }
  });

  return obj;
}
