import * as fs from 'fs';

import { buildClientSchema, Source, concatAST, parse, DocumentNode, GraphQLSchema, buildASTSchema } from 'graphql';

import { ToolError } from './errors';
import { extname, join, normalize, relative } from 'path';

export function loadSchema(schemaPath: string): GraphQLSchema {
  if (extname(schemaPath) === '.json') {
    return loadIntrospectionSchema(schemaPath);
  }
  return loadSDLSchema(schemaPath);
}

function loadIntrospectionSchema(schemaPath: string): GraphQLSchema {
  if (!fs.existsSync(schemaPath)) {
    throw new ToolError(`Cannot find GraphQL schema file: ${schemaPath}`);
  }
  const schemaData = require(schemaPath);

  if (!schemaData.data && !schemaData.__schema) {
    throw new ToolError('GraphQL schema file should contain a valid GraphQL introspection query result');
  }
  return buildClientSchema(schemaData.data ? schemaData.data : schemaData);
}

function loadSDLSchema(schemaPath: string): GraphQLSchema {
  const authDirectivePath = normalize(join(__dirname, '..', 'awsAppSyncDirectives.graphql'));
  const doc = loadAndMergeQueryDocuments([authDirectivePath, schemaPath]);
  return buildASTSchema(doc);
}
function extractDocumentFromJavascript(content: string, tagName: string = 'gql'): string | null {
  const re = new RegExp(tagName + '\\s*`([^`/]*)`', 'g');

  let match;
  const matches = [];

  while ((match = re.exec(content))) {
    const doc = match[1].replace(/\${[^}]*}/g, '');

    matches.push(doc);
  }

  const doc = matches.join('\n');
  return doc.length ? doc : null;
}

export function loadAndMergeQueryDocuments(inputPaths: string[], tagName: string = 'gql'): DocumentNode {
  const sources = inputPaths
    .map(inputPath => {
      const body = fs.readFileSync(inputPath, 'utf8');
      if (!body) {
        return null;
      }

      if (inputPath.endsWith('.jsx') || inputPath.endsWith('.js') || inputPath.endsWith('.tsx') || inputPath.endsWith('.ts')) {
        const doc = extractDocumentFromJavascript(body.toString(), tagName);
        return doc ? new Source(doc, inputPath) : null;
      }

      return new Source(body, inputPath);
    })
    .filter((source): source is Source => Boolean(source));

  const parsedSources = sources.map(source => {
    try {
      return parse(source);
    } catch (err) {
      const relativePathToInput = relative(process.cwd(), source.name);
      throw new ToolError(`Could not parse graphql operations in ${relativePathToInput}\n  Failed on : ${source.body}`);
    }
  });
  return concatAST(parsedSources);
}