
import type { getDirective } from "@graphql-tools/utils";

import {
  type GraphQLOutputType, type GraphQLSchema, isOutputType,
} from "graphql";

import { Plugin } from "./Plugin";
import {
  type IntrospectionKind, IntrospectionType, type SchemaMapperArgs,
  createDirective, getExtension, patchField, setExtension,
} from "../utilities";

export namespace withMetadata {
  export type Fallback = (
    value: Parameters<typeof getDirective>[1], name: string
  ) => unknown;

  export type Fields = Record<IntrospectionKind | "default", Record<string, string>>;
  export type Names = Partial<Record<IntrospectionKind, string>>;
  export type Fallbacks = Partial<Record<IntrospectionKind, Fallback>>;

  export interface Options {
    fields    : Fields;
    names?    : Names;
    fallbacks?: Fallbacks;
  }
}

export function withMetadata ( options: withMetadata.Options ) {
  const plugins: Array<Plugin> = [ ];

  for (const key in options.fields) {
    if ("default" !== key) {
      const kind = key as IntrospectionKind;

      plugins.push(createDirective(
        options.names?.[kind] ?? `${IntrospectionType[kind].name.slice(2)}Metadata`,
        { ...options.fields.default, ...options.fields[kind], },
        { [kind]: directiveMapper, }
      ));
    }
  }

  plugins.push({
    transformer: {
      [Plugin.Phase.FINALIZE] ( schema ) {
        for (const name in options.fields) {
          if ("default" !== name) {
            addFields(schema, name as IntrospectionKind, options);
          }
        }

        return schema;
      },
    },
  });

  return plugins;
}

function directiveMapper (
  [ directive, ]: Array<Record<string, unknown>>,
  type: SchemaMapperArgs[0]
) {
  setExtension(type, "metadata", directive);
}

function addFields (
  schema: GraphQLSchema, kind: IntrospectionKind, options: withMetadata.Options
) {
  const field = options.fields[kind];

  for (const key in field) {
    const type = schema.getType(field[key]!);

    if (!type || !isOutputType(type)) {
      throw new TypeError(`MetadataPlugin expects "GraphQLOutputType" received "${type?.name}"`);
    }

    addField(kind, key, type, options);
  }
}

function addField (
  kind: IntrospectionKind, key: string,
  type: GraphQLOutputType, options: withMetadata.Options
) {
  const fallback = options.fallbacks?.[kind] || alwaysNull;

  patchField(IntrospectionType[kind], key, {
    type,
    resolve ( source: Parameters<withMetadata.Fallback>[0] ) {
      const value = getExtension<Record<string, unknown>>(source, "metadata")?.[key];

      return value ?? fallback(source, key);
    },
  });
}

function alwaysNull ( ): null { return null; }
