/**
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */

'use strict';

const SchemaUtils = require('../../core/SchemaUtils');

const {createUserError} = require('../../core/RelayCompilerError');
const {
  buildFragmentSpread,
  buildOperationArgumentDefinitions,
} = require('./utils');

import type {Fragment} from '../../core/GraphQLIR';
import type {Schema} from '../../core/Schema';
import type {QueryGenerator, RefetchRoot} from '.';

const NODE_TYPE_NAME = 'Node';
const NODE_FIELD_NAME = 'node';

function buildRefetchOperation(
  schema: Schema,
  fragment: Fragment,
  queryName: string,
): ?RefetchRoot {
  const eligible =
    schema.getTypeString(fragment.type) === NODE_TYPE_NAME ||
    (schema.isObject(fragment.type) &&
      schema
        .getInterfaces(schema.assertCompositeType(fragment.type))
        .some(interfaceType =>
          schema.areEqualTypes(
            interfaceType,
            schema.expectTypeFromString(NODE_TYPE_NAME),
          ),
        )) ||
    (schema.isAbstractType(fragment.type) &&
      Array.from(
        schema.getPossibleTypes(schema.assertAbstractType(fragment.type)),
      ).every(possibleType =>
        schema.implementsInterface(
          schema.assertCompositeType(possibleType),
          schema.assertInterfaceType(
            schema.expectTypeFromString(NODE_TYPE_NAME),
          ),
        ),
      ));
  if (!eligible) {
    return null;
  }

  const queryType = schema.expectQueryType();
  const nodeType = schema.getTypeFromString(NODE_TYPE_NAME);
  const nodeField = schema.getFieldConfig(
    schema.expectField(queryType, NODE_FIELD_NAME),
  );
  if (
    !(
      nodeType &&
      schema.isInterface(nodeType) &&
      schema.isInterface(nodeField.type) &&
      schema.areEqualTypes(nodeField.type, nodeType) &&
      nodeField.args.length === 1 &&
      schema.areEqualTypes(
        schema.getNullableType(nodeField.args[0].type),
        schema.expectIdType(),
      ) &&
      // the fragment must be on Node or on a type that implements Node
      ((schema.isObject(fragment.type) &&
        schema
          .getInterfaces(schema.assertCompositeType(fragment.type))
          .some(interfaceType =>
            schema.areEqualTypes(interfaceType, nodeType),
          )) ||
        (schema.isAbstractType(fragment.type) &&
          Array.from(
            schema.getPossibleTypes(schema.assertAbstractType(fragment.type)),
          ).every(possibleType =>
            schema
              .getInterfaces(schema.assertCompositeType(possibleType))
              .some(interfaceType =>
                schema.areEqualTypes(interfaceType, nodeType),
              ),
          )))
    )
  ) {
    throw createUserError(
      `Invalid use of @refetchable on fragment '${fragment.name}', check ` +
        'that your schema defines a `Node { id: ID }` interface and has a ' +
        '`node(id: ID): Node` field on the query type (the id argument may ' +
        'also be non-null).',
      [fragment.loc],
    );
  }

  // name and type of the node(_: ID) field parameter
  const idArgName = nodeField.args[0].name;
  const idArgType = nodeField.args[0].type;
  // name and type of the query variable
  const idVariableType = SchemaUtils.getNonNullIdInput(schema);
  const idVariableName = 'id';

  const argumentDefinitions = buildOperationArgumentDefinitions(
    fragment.argumentDefinitions,
  );
  const idArgument = argumentDefinitions.find(
    argDef => argDef.name === idVariableName,
  );
  if (idArgument != null) {
    throw createUserError(
      `Invalid use of @refetchable on fragment \`${fragment.name}\`, this ` +
        'fragment already has an `$id` variable in scope.',
      [idArgument.loc],
    );
  }
  const argumentDefinitionsWithId = [
    ...argumentDefinitions,
    {
      defaultValue: null,
      kind: 'LocalArgumentDefinition',
      loc: {kind: 'Derived', source: fragment.loc},
      name: idVariableName,
      type: idVariableType,
    },
  ];
  return {
    path: [NODE_FIELD_NAME],
    node: {
      argumentDefinitions: argumentDefinitionsWithId,
      directives: [],
      kind: 'Root',
      loc: {kind: 'Derived', source: fragment.loc},
      metadata: null,
      name: queryName,
      operation: 'query',
      selections: [
        {
          alias: NODE_FIELD_NAME,
          args: [
            {
              kind: 'Argument',
              loc: {kind: 'Derived', source: fragment.loc},
              name: idArgName,
              type: schema.assertInputType(idArgType),
              value: {
                kind: 'Variable',
                loc: {kind: 'Derived', source: fragment.loc},
                variableName: idVariableName,
                type: idVariableType,
              },
            },
          ],
          connection: false,
          directives: [],
          handles: null,
          kind: 'LinkedField',
          loc: {kind: 'Derived', source: fragment.loc},
          metadata: null,
          name: NODE_FIELD_NAME,
          selections: [buildFragmentSpread(fragment)],
          type: schema.assertLinkedFieldType(nodeType),
        },
      ],
      type: queryType,
    },
    transformedFragment: enforceIDField(schema, fragment),
  };
}

function enforceIDField(schema: Schema, fragment: Fragment): Fragment {
  const idSelection = fragment.selections.find(
    selection =>
      selection.kind === 'ScalarField' &&
      selection.name === 'id' &&
      selection.alias === 'id' &&
      schema.areEqualTypes(
        schema.getNullableType(selection.type),
        schema.expectIdType(),
      ),
  );
  if (idSelection) {
    return fragment;
  }
  return {
    ...fragment,
    selections: [
      ...fragment.selections,
      SchemaUtils.generateIDField(schema.expectIdType()),
    ],
  };
}

module.exports = ({
  description: 'the Node interface or types implementing the Node interface',
  buildRefetchOperation,
}: QueryGenerator);
