import {
  ObjectTypeDefinitionNode,
  InputValueDefinitionNode,
  FieldDefinitionNode,
  TypeNode,
  SchemaDefinitionNode,
  OperationTypeNode,
  OperationTypeDefinitionNode,
  ObjectTypeExtensionNode,
  NamedTypeNode,
  Kind,
  NonNullTypeNode,
  ListTypeNode,
  valueFromASTUntyped,
  ArgumentNode,
  DirectiveNode,
  EnumTypeDefinitionNode,
  ValueNode,
  InputObjectTypeDefinitionNode,
  UnionTypeDefinitionNode,
  DocumentNode,
  DefinitionNode,
} from 'graphql';

type ScalarMap = {
  [k: string]: 'String' | 'Int' | 'Float' | 'Boolean' | 'ID';
};
export const STANDARD_SCALARS: ScalarMap = {
  String: 'String',
  Int: 'Int',
  Float: 'Float',
  Boolean: 'Boolean',
  ID: 'ID',
};

const OTHER_SCALARS: ScalarMap = {
  BigInt: 'Int',
  Double: 'Float',
};

export const APPSYNC_DEFINED_SCALARS: ScalarMap = {
  AWSDate: 'String',
  AWSTime: 'String',
  AWSDateTime: 'String',
  AWSTimestamp: 'Int',
  AWSEmail: 'String',
  AWSJSON: 'String',
  AWSURL: 'String',
  AWSPhone: 'String',
  AWSIPAddress: 'String',
};

export const DEFAULT_SCALARS: ScalarMap = {
  ...STANDARD_SCALARS,
  ...OTHER_SCALARS,
  ...APPSYNC_DEFINED_SCALARS,
};

export const NUMERIC_SCALARS: { [k: string]: boolean } = {
  BigInt: true,
  Int: true,
  Float: true,
  Double: true,
  AWSTimestamp: true,
};

export const MAP_SCALARS: { [k: string]: boolean } = {
  AWSJSON: true,
};

export function attributeTypeFromScalar(scalar: TypeNode): 'S' | 'N' {
  const baseType = getBaseType(scalar);
  const baseScalar = DEFAULT_SCALARS[baseType];
  if (!baseScalar) {
    throw new Error(`Expected scalar and got ${baseType}`);
  }
  switch (baseScalar) {
    case 'String':
    case 'ID':
      return 'S';
    case 'Int':
    case 'Float':
      return 'N';
    case 'Boolean':
      throw new Error(`Boolean values cannot be used as sort keys.`);
    default:
      throw new Error(`There is no valid DynamoDB attribute type for scalar ${baseType}`);
  }
}

export function isScalar(type: TypeNode) {
  if (type.kind === Kind.NON_NULL_TYPE) {
    return isScalar(type.type);
  } else if (type.kind === Kind.LIST_TYPE) {
    return isScalar(type.type);
  } else {
    return Boolean(DEFAULT_SCALARS[type.name.value]);
  }
}

export function isScalarOrEnum(type: TypeNode, enums: EnumTypeDefinitionNode[]) {
  if (type.kind === Kind.NON_NULL_TYPE) {
    return isScalarOrEnum(type.type, enums);
  } else if (type.kind === Kind.LIST_TYPE) {
    return isScalarOrEnum(type.type, enums);
  } else {
    for (const e of enums) {
      if (e.name.value === type.name.value) {
        return true;
      }
    }
    return Boolean(DEFAULT_SCALARS[type.name.value]);
  }
}

export const isArrayOrObject = (type: TypeNode, enums: EnumTypeDefinitionNode[]): boolean => {
  if (type.kind === Kind.NON_NULL_TYPE) {
    return isArrayOrObject(type.type, enums);
  } else if (type.kind === Kind.LIST_TYPE) {
    return true;
  } else if (enums.some((e) => e.name.value === type.name.value)) {
    return false;
  } else {
    return !DEFAULT_SCALARS[type.name.value];
  }
};

export function isEnum(type: TypeNode, document: DocumentNode) {
  const baseType = getBaseType(type);
  return document.definitions.find((def) => {
    return def.kind === Kind.ENUM_TYPE_DEFINITION && def.name.value === baseType;
  });
}

export function getBaseType(type: TypeNode): string {
  if (type.kind === Kind.NON_NULL_TYPE) {
    return getBaseType(type.type);
  } else if (type.kind === Kind.LIST_TYPE) {
    return getBaseType(type.type);
  } else {
    return type.name.value;
  }
}

export function isListType(type: TypeNode): boolean {
  if (type.kind === Kind.NON_NULL_TYPE) {
    return isListType(type.type);
  } else if (type.kind === Kind.LIST_TYPE) {
    return true;
  } else {
    return false;
  }
}

export function isNonNullType(type: TypeNode): boolean {
  return type.kind === Kind.NON_NULL_TYPE;
}

export function getDirectiveArgument(directive: DirectiveNode, arg: string, dflt?: any) {
  const argument = directive.arguments.find((a) => a.name.value === arg);
  return argument ? valueFromASTUntyped(argument.value) : dflt;
}

export function unwrapNonNull(type: TypeNode) {
  if (type.kind === 'NonNullType') {
    return unwrapNonNull(type.type);
  }
  return type;
}

export function wrapNonNull(type: TypeNode) {
  if (type.kind !== 'NonNullType') {
    return makeNonNullType(type);
  }
  return type;
}

export function makeOperationType(operation: OperationTypeNode, type: string): OperationTypeDefinitionNode {
  return {
    kind: 'OperationTypeDefinition',
    operation,
    type: {
      kind: 'NamedType',
      name: {
        kind: 'Name',
        value: type,
      },
    },
  };
}

export function makeSchema(operationTypes: OperationTypeDefinitionNode[]): SchemaDefinitionNode {
  return {
    kind: Kind.SCHEMA_DEFINITION,
    operationTypes,
    directives: [],
  };
}

export function blankObject(name: string): ObjectTypeDefinitionNode {
  return {
    kind: 'ObjectTypeDefinition',
    name: {
      kind: 'Name',
      value: name,
    },
    fields: [],
    directives: [],
    interfaces: [],
  };
}

export function blankObjectExtension(name: string): ObjectTypeExtensionNode {
  return {
    kind: Kind.OBJECT_TYPE_EXTENSION,
    name: {
      kind: 'Name',
      value: name,
    },
    fields: [],
    directives: [],
    interfaces: [],
  };
}

export function extensionWithFields(object: ObjectTypeExtensionNode, fields: FieldDefinitionNode[]): ObjectTypeExtensionNode {
  return {
    ...object,
    fields: [...object.fields, ...fields],
  };
}

export function extensionWithDirectives(object: ObjectTypeExtensionNode, directives: DirectiveNode[]): ObjectTypeExtensionNode {
  if (directives && directives.length > 0) {
    const newDirectives = [];

    for (const directive of directives) {
      if (!object.directives.find((d) => d.name.value === directive.name.value)) {
        newDirectives.push(directive);
      }
    }

    if (newDirectives.length > 0) {
      return {
        ...object,
        directives: [...object.directives, ...newDirectives],
      };
    }
  }

  return object;
}

export function extendFieldWithDirectives(field: FieldDefinitionNode, directives: DirectiveNode[]): FieldDefinitionNode {
  if (directives && directives.length > 0) {
    const newDirectives = [];

    for (const directive of directives) {
      if (!field.directives.find((d) => d.name.value === directive.name.value)) {
        newDirectives.push(directive);
      }
    }

    if (newDirectives.length > 0) {
      return {
        ...field,
        directives: [...field.directives, ...newDirectives],
      };
    }
  }

  return field;
}

export function defineUnionType(name: string, types: NamedTypeNode[] = []): UnionTypeDefinitionNode {
  return {
    kind: Kind.UNION_TYPE_DEFINITION,
    name: {
      kind: 'Name',
      value: name,
    },
    types: types,
  };
}

export function makeInputObjectDefinition(name: string, inputs: InputValueDefinitionNode[]): InputObjectTypeDefinitionNode {
  return {
    kind: 'InputObjectTypeDefinition',
    name: {
      kind: 'Name',
      value: name,
    },
    fields: inputs,
    directives: [],
  };
}

export function makeObjectDefinition(name: string, inputs: FieldDefinitionNode[]): ObjectTypeDefinitionNode {
  return {
    kind: Kind.OBJECT_TYPE_DEFINITION,
    name: {
      kind: 'Name',
      value: name,
    },
    fields: inputs,
    directives: [],
  };
}

export function makeField(
  name: string,
  args: InputValueDefinitionNode[],
  type: TypeNode,
  directives: DirectiveNode[] = [],
): FieldDefinitionNode {
  return {
    kind: Kind.FIELD_DEFINITION,
    name: {
      kind: 'Name',
      value: name,
    },
    arguments: args,
    type,
    directives,
  };
}

export function makeDirective(name: string, args: ArgumentNode[]): DirectiveNode {
  return {
    kind: Kind.DIRECTIVE,
    name: {
      kind: Kind.NAME,
      value: name,
    },
    arguments: args,
  };
}

export function makeArgument(name: string, value: ValueNode): ArgumentNode {
  return {
    kind: Kind.ARGUMENT,
    name: {
      kind: 'Name',
      value: name,
    },
    value,
  };
}

export function makeValueNode(value: any): ValueNode {
  if (typeof value === 'string') {
    return { kind: Kind.STRING, value: value };
  } else if (Number.isInteger(value)) {
    return { kind: Kind.INT, value: value };
  } else if (typeof value === 'number') {
    return { kind: Kind.FLOAT, value: String(value) };
  } else if (typeof value === 'boolean') {
    return { kind: Kind.BOOLEAN, value: value };
  } else if (value === null) {
    return { kind: Kind.NULL };
  } else if (Array.isArray(value)) {
    return {
      kind: Kind.LIST,
      values: value.map((v) => makeValueNode(v)),
    };
  } else if (typeof value === 'object') {
    return {
      kind: Kind.OBJECT,
      fields: Object.keys(value).map((key: string) => {
        const keyValNode = makeValueNode(value[key]);
        return {
          kind: Kind.OBJECT_FIELD,
          name: { kind: Kind.NAME, value: key },
          value: keyValNode,
        };
      }),
    };
  }
}

export function makeInputValueDefinition(name: string, type: TypeNode): InputValueDefinitionNode {
  return {
    kind: Kind.INPUT_VALUE_DEFINITION,
    name: {
      kind: 'Name',
      value: name,
    },
    type,
    directives: [],
  };
}

export function makeNamedType(name: string): NamedTypeNode {
  return {
    kind: 'NamedType',
    name: {
      kind: 'Name',
      value: name,
    },
  };
}

export function makeNonNullType(type: NamedTypeNode | ListTypeNode): NonNullTypeNode {
  return {
    kind: Kind.NON_NULL_TYPE,
    type,
  };
}

export function makeListType(type: TypeNode): ListTypeNode {
  return {
    kind: 'ListType',
    type,
  };
}

export const findObjectDefinition = (document: DocumentNode, name: string): ObjectTypeDefinitionNode | undefined => {
  return document.definitions?.find((def) => def?.kind === 'ObjectTypeDefinition' && def?.name?.value === name) as
    | ObjectTypeDefinitionNode
    | undefined;
};

export const isNamedType = (type: TypeNode): boolean => {
  return type?.kind === Kind.NAMED_TYPE || (type?.kind === Kind.NON_NULL_TYPE && type?.type?.kind === Kind.NAMED_TYPE);
};

export const getNonModelTypes = (document: DocumentNode): DefinitionNode[] => {
  const nonModels = document.definitions?.filter((def) => isNonModelType(def));
  return nonModels;
};

export const isNonModelType = (definition: DefinitionNode) => {
  return definition?.kind === 'ObjectTypeDefinition' && !directiveExists(definition, 'model');
};

export const directiveExists = (definition: ObjectTypeDefinitionNode, name: string) => {
  return definition?.directives?.find((directive) => directive?.name?.value === name);
};

export const isOfType = (type: TypeNode, name: string): boolean => {
  if (type.kind === Kind.NON_NULL_TYPE) {
    return isOfType(type?.type, name);
  }

  if (!isNamedType(type)) {
    return false;
  }

  return (type as NamedTypeNode).name?.value === name;
};
