type UpdateGQLResult<T> = {
  [K in keyof T]?: (keyof T[K] | UpdateGQLResult<T[K]> | string)[] | keyof T[K];
};

type UpdateGQLFragmentGenerator<T> = <U, Y = undefined>(
  mutation: keyof T,
  args: CustomArgs<U>,
  result?: (keyof Y | UpdateGQLResult<Y>)[],
) => string;

export interface UpdateGQLFragmentEnumInput {
  type: 'enum';
  value: unknown;
}

type CustomArgs<T> = {
  [P in keyof T]: T[P] extends (infer U)[]
    ? CustomArgs<U>[] // eslint-disable-next-line @typescript-eslint/ban-types
    : T[P] extends object
    ? CustomArgs<T[P]>
    : T[P] | UpdateGQLFragmentEnumInput;
};

/**
 * Factory for creating a `generateUpdateGQLFragment` function that can be used to
 * generate GraphQL fragments for the mutations present in `T`.
 * The generic `T` should usually be set to the `Mutation` type generated by the
 * GraphQL Code Generator.
 */
export function createUpdateGQLFragmentGenerator<
  T,
>(): UpdateGQLFragmentGenerator<T> {
  return <U, Y = undefined>(
    mutation: keyof T,
    args: CustomArgs<U>,
    result?: (keyof Y | UpdateGQLResult<Y>)[],
  ): string => {
    const enumValues: string[] = [];

    let GqlFragment = `${String(mutation)} (${JSON.stringify(
      args,
      (_key, value) => {
        if (value && typeof value === 'object') {
          Object.keys(value).map((v) => {
            if (value[v] && isEnumType(value[v])) {
              enumValues.push(
                (value[v] as UpdateGQLFragmentEnumInput).value as string,
              );
              value[v] = (value[v] as UpdateGQLFragmentEnumInput).value;
            }
          });
        }
        return value;
      },
    )
      // Regex to remove the quotes from properties https://stackoverflow.com/questions/11233498/json-stringify-without-quotes-on-properties
      .replace(/"([^\\"]+)":/g, '$1:')
      // Remove the curly braces from beginning and end
      .slice(1, -1)}) {${getResult(result)}}`;

    enumValues.forEach((value) => {
      GqlFragment = GqlFragment.replace(`"${value}"`, `${value}`);
    });

    return GqlFragment;
  };
}

function isEnumType(object: unknown): object is UpdateGQLFragmentEnumInput {
  return (object as UpdateGQLFragmentEnumInput).type === 'enum';
}

/**
 * Generates a string representing the desired result fields for a GraphQL query or mutation.
 * Supports both simple keys and nested UpdateGQLResult objects for recursive results.
 * @param result - An optional array of keys (strings) or an array of UpdateGQLResult objects.
 * If not provided, the function will default to returning 'clientMutationId'.
 * @returns A string representing the result fields for a GraphQL query or mutation.
 */
const getResult = <Y>(
  result?: (keyof Y | UpdateGQLResult<Y>)[] | Y,
): string => {
  // If no result is provided, return the default 'clientMutationId'
  if (!result) {
    return 'clientMutationId';
  }
  // If the result is an array, process it
  else if (Array.isArray(result)) {
    const results: string[] = [];

    // Iterate through each item in the result array
    result.forEach((item) => {
      // If the item is a string, add it directly to the results array
      if (typeof item === 'string') {
        results.push(item);
      }
      // If the item is an object, recursively process its keys and values
      else {
        const keys = Object.keys(item) as (keyof Y)[];
        keys.forEach((key) => {
          const value = (item as Y)[key];
          // If the value is an object, process it recursively using getResult
          if (typeof value === 'object') {
            results.push(`${String(key)} {${getResult(value)}}`);
          }
          // If the value is not an object, add the key directly to the results array
          else {
            results.push(String(key));
          }
        });
      }
    });

    // Join the results array into a single string and return it
    return results.join(' ');
  }
  // If the result is an object, process it
  else {
    const keys = Object.keys(result) as (keyof Y)[];
    const results: string[] = [];

    // Iterate through each key in the object
    keys.forEach((key) => {
      const value = (result as Y)[key];
      // If the value is an object, process it recursively using getResult
      if (typeof value === 'object') {
        results.push(`${String(key)} {${getResult(value)}}`);
      }
      // If the value is not an object, add the key directly to the results array
      else {
        results.push(String(key));
      }
    });

    // Join the results array into a single string and return it
    return results.join(' ');
  }
};
