
import type { Plugin } from "../plugins/Plugin";

import {
  MapperKind, type SchemaMapper, getDirective, mapSchema,
} from "@graphql-tools/utils";
import { isDirective, isSchema } from "graphql";

import { parseLiteral } from "./parse";

export type SchemaMapperType<Kind extends MapperKind | null = null> =
  SchemaMapperArgs<Kind>[0];

export type SchemaMapperTypes = {
  [Kind in MapperKind]: SchemaMapperType<Kind>;
};

export type SchemaMapperArgs<Kind extends MapperKind | null = null> =
  Parameters<NonNullable<Kind extends MapperKind
    ? SchemaMapper[Kind] : SchemaMapper[keyof SchemaMapper]>>;

export type SchemaMapperFunction<Kind extends MapperKind | null = null> = (
  directives: Array<Record<string, unknown>>,
  ...original: SchemaMapperArgs<Kind>
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
) => SchemaMapperType<Kind> | null | undefined | void;

export type SchemaMapperExtended = {
  [Kind in MapperKind]: SchemaMapperFunction<Kind>;
};

const Location: { [Key in MapperKind]: Array<string>; } = {
  [MapperKind.TYPE]                   : [ "SCALAR", "OBJECT", "INTERFACE", "UNION", "ENUM", "INPUT_OBJECT", ],
  [MapperKind.SCALAR_TYPE]            : [ "SCALAR", ],
  [MapperKind.ENUM_TYPE]              : [ "ENUM", ],
  [MapperKind.COMPOSITE_TYPE]         : [ "OBJECT", "INTERFACE", "UNION", ],
  [MapperKind.OBJECT_TYPE]            : [ "OBJECT", ],
  [MapperKind.INPUT_OBJECT_TYPE]      : [ "INPUT_OBJECT", ],
  [MapperKind.ABSTRACT_TYPE]          : [ "INTERFACE", "UNION", ],
  [MapperKind.UNION_TYPE]             : [ "UNION_TYPE", ],
  [MapperKind.INTERFACE_TYPE]         : [ "INTERFACE", ],
  [MapperKind.ROOT_OBJECT]            : [ "SCHEMA", ],
  [MapperKind.QUERY]                  : [ "OBJECT", ],
  [MapperKind.MUTATION]               : [ "OBJECT", ],
  [MapperKind.SUBSCRIPTION]           : [ "OBJECT", ],
  [MapperKind.ENUM_VALUE]             : [ "ENUM_VALUE", ],
  [MapperKind.FIELD]                  : [ "FIELD_DEFINITION", "INPUT_FIELD_DEFINITION", ],
  [MapperKind.OBJECT_FIELD]           : [ "FIELD_DEFINITION", ],
  [MapperKind.ROOT_FIELD]             : [ "FIELD_DEFINITION", ],
  [MapperKind.QUERY_ROOT_FIELD]       : [ "FIELD_DEFINITION", ],
  [MapperKind.MUTATION_ROOT_FIELD]    : [ "FIELD_DEFINITION", ],
  [MapperKind.SUBSCRIPTION_ROOT_FIELD]: [ "FIELD_DEFINITION", ],
  [MapperKind.INTERFACE_FIELD]        : [ "FIELD_DEFINITION", ],
  [MapperKind.COMPOSITE_FIELD]        : [ "FIELD_DEFINITION", ],
  [MapperKind.ARGUMENT]               : [ "ARUGMENT_DEFINITION", ],
  [MapperKind.INPUT_OBJECT_FIELD]     : [ "INPUT_FIELD_DEFINITION", ],
  [MapperKind.DIRECTIVE]              : [ "DIRECTIVE", ],
};

function gatherLocations ( keys: Array<MapperKind> ) {
  const locations = new Set<string>();

  for (const key of keys) {
    for (const location of Location[key]) {
      locations.add(location);
    }
  }

  return ([ ...locations, ]).join("|");
}

export function createDirective (
  name: string, args: Record<string, string>,
  mapper: Partial<SchemaMapperExtended>
) {
  const argList = Object.entries(args)
    .map(function mapArg ( [ key, value, ] ) {
      return `${key}:${value}`;
    }).join(",");
  const argStruct = argList ? `(${argList})` : "";

  return {
    typeDefs: `directive @${name}${argStruct} on ${
      gatherLocations(Object.keys(mapper) as Array<MapperKind>)
    }`,
    transformer ( schema ) {
      return mapSchema(schema, wrapSchemaMapper(name, mapper));
    },
  } as const as Plugin;
}

function wrapSchemaMapper (
  name: string, mapper: Partial<SchemaMapperExtended>
) {
  const wrapped: Record<string, unknown> = { };

  for (const key in mapper) {
    wrapped[key] = wrapMapper(
      name, mapper[key as keyof SchemaMapperExtended]! as SchemaMapperFunction
    );
  }

  return wrapped as SchemaMapper;
}

function wrapMapper ( name: string, mapperFunction: SchemaMapperFunction ) {
  return function mapper ( ...args: SchemaMapperArgs ) {
    const [ type, ] = args;

    if (isDirective(type)) {
      return mapperFunction([ ], ...args) ?? type;
    }

    const directives = getDirective(
      (args as Array<unknown>).find(isSchema)!, type, name
    ) ?? getAstDirective(type, name);

    if (directives?.[0]) {
      return mapperFunction(directives, ...args) ?? type;
    }

    return type;
  };
}

function getAstDirective ( type: SchemaMapperType, name: string ) {
  if (type.astNode && "directives" in type.astNode && type.astNode.directives) {
    return type.astNode.directives.filter(function filterDir ( directive ) {
      return directive.name.value === name;
    }).map(function mapDir ( directive ) {
      return directive.arguments
        ?.reduce<Record<string, unknown>>(function reduceArg ( acc, arg ) {
        const { name: { value: argName, }, value, } = arg;

        acc[argName] = parseLiteral("value" in value ? String(value.value) : "null", value);

        return acc;
      }, { }) ?? { };
    });
  }

  return [ ];
}
