import { useEnv } from '@directus/env';
import type { Accountability, Item, Query, SchemaOverview } from '@directus/types';
import type { ExecutionResult, FormattedExecutionResult, GraphQLSchema } from 'graphql';
import { NoSchemaIntrospectionCustomRule, execute, specifiedRules, validate } from 'graphql';
import type { Knex } from 'knex';
import getDatabase from '../../database/index.js';
import type { AbstractServiceOptions, GraphQLParams } from '../../types/index.js';
import { getService } from '../../utils/get-service.js';
import { formatError } from './errors/format.js';
import { GraphQLExecutionError, GraphQLValidationError } from './errors/index.js';
import { generateSchema } from './schema/index.js';
import { addPathToValidationError } from './utils/add-path-to-validation-error.js';
import processError from './utils/process-error.js';

export type GQLScope = 'items' | 'system';

const env = useEnv();

const validationRules = Array.from(specifiedRules);

if (env['GRAPHQL_INTROSPECTION'] === false) {
	validationRules.push(NoSchemaIntrospectionCustomRule);
}

export class GraphQLService {
	accountability: Accountability | null;
	knex: Knex;
	schema: SchemaOverview;
	scope: GQLScope;

	constructor(options: AbstractServiceOptions & { scope: GQLScope }) {
		this.accountability = options?.accountability || null;
		this.knex = options?.knex || getDatabase();
		this.schema = options.schema;
		this.scope = options.scope;
	}

	/**
	 * Execute a GraphQL structure
	 */
	async execute({
		document,
		variables,
		operationName,
		contextValue,
	}: GraphQLParams): Promise<FormattedExecutionResult> {
		const schema = await this.getSchema();

		const validationErrors = validate(schema, document, validationRules).map((validationError) =>
			addPathToValidationError(validationError),
		);

		if (validationErrors.length > 0) {
			throw new GraphQLValidationError({ errors: validationErrors });
		}

		let result: ExecutionResult;

		try {
			result = await execute({
				schema,
				document,
				contextValue,
				variableValues: variables,
				operationName,
			});
		} catch (err: any) {
			throw new GraphQLExecutionError({ errors: [err.message] });
		}

		const formattedResult: FormattedExecutionResult = {};

		if (result['data']) formattedResult.data = result['data'];

		if (result['errors']) {
			formattedResult.errors = result['errors'].map((error) => processError(this.accountability, error));
		}

		if (result['extensions']) formattedResult.extensions = result['extensions'];

		return formattedResult;
	}

	/**
	 * Generate the GraphQL schema. Pulls from the schema information generated by the get-schema util.
	 */
	async getSchema(): Promise<GraphQLSchema>;
	async getSchema(type: 'schema'): Promise<GraphQLSchema>;
	async getSchema(type: 'sdl'): Promise<GraphQLSchema | string>;
	async getSchema(type: 'schema' | 'sdl' = 'schema'): Promise<GraphQLSchema | string> {
		return generateSchema(this, type);
	}

	/**
	 * Execute the read action on the correct service. Checks for singleton as well.
	 */
	async read(collection: string, query: Query): Promise<Partial<Item>> {
		const service = getService(collection, {
			knex: this.knex,
			accountability: this.accountability,
			schema: this.schema,
		});

		const result = this.schema.collections[collection]!.singleton
			? await service.readSingleton(query, { stripNonRequested: false })
			: await service.readByQuery(query, { stripNonRequested: false });

		return result;
	}

	/**
	 * Upsert and read singleton item
	 */
	async upsertSingleton(
		collection: string,
		body: Record<string, any> | Record<string, any>[],
		query: Query,
	): Promise<Partial<Item> | boolean> {
		const service = getService(collection, {
			knex: this.knex,
			accountability: this.accountability,
			schema: this.schema,
		});

		try {
			await service.upsertSingleton(body);

			if ((query.fields || []).length > 0) {
				const result = await service.readSingleton(query);
				return result;
			}

			return true;
		} catch (err: any) {
			throw formatError(err);
		}
	}
}
