from pathlib import Path
from textwrap import dedent


def client_typescript_codegen_config(
    app_name: str,
    schema_path: Path,
    fragments_dir: Path,
    mutations_dir: Path,
    queries_dir: Path,
    client_dir: Path,
):
    return dedent(f"""\
const headerText = `
// This file is automatically generated. DO NOT EDIT MANUALLY.

/* eslint-disable */
`.trim();

const fragmentMasking = `
// This file is automatically generated. DO NOT EDIT MANUALLY.

/* eslint-disable */
import type {{ ResultOf, DocumentTypeDecoration, TypedDocumentNode }} from '@graphql-typed-document-node/core';
import type {{ FragmentDefinitionNode }} from 'graphql';

// Brand a fragment reference
export type FragmentType<TDocumentType extends DocumentTypeDecoration<any, any>> =
  TDocumentType extends DocumentTypeDecoration<infer TType, any>
    ? [TType] extends [{{ ' $fragmentName'?: infer TKey }}]
      ? TKey extends string ? {{ ' $fragmentRefs'?: {{ [key in TKey]: TType }} }} : never
      : never
    : never;

// Wrap real data as a fragment reference when needed
export function makeFragmentData<
  F extends DocumentTypeDecoration<any, any>,
  FT extends ResultOf<F>
>(data: FT, _fragment: F): FragmentType<F> {{
  return data as FragmentType<F>;
}}

// Overloads (non-nullable, nullable, arrays, readonly arrays)
export function useFragment<TType>(
  _documentNode: DocumentTypeDecoration<TType, any>,
  fragmentRef: FragmentType<DocumentTypeDecoration<TType, any>>
): TType;
export function useFragment<TType>(
  _documentNode: DocumentTypeDecoration<TType, any>,
  fragmentRef: FragmentType<DocumentTypeDecoration<TType, any>> | undefined
): TType | undefined;
export function useFragment<TType>(
  _documentNode: DocumentTypeDecoration<TType, any>,
  fragmentRef: FragmentType<DocumentTypeDecoration<TType, any>> | null
): TType | null;
export function useFragment<TType>(
  _documentNode: DocumentTypeDecoration<TType, any>,
  fragmentRef: FragmentType<DocumentTypeDecoration<TType, any>> | null | undefined
): TType | null | undefined;
export function useFragment<TType>(
  _documentNode: DocumentTypeDecoration<TType, any>,
  fragmentRef: Array<FragmentType<DocumentTypeDecoration<TType, any>>>
): Array<TType>;
export function useFragment<TType>(
  _documentNode: DocumentTypeDecoration<TType, any>,
  fragmentRef: Array<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): Array<TType> | null | undefined;
export function useFragment<TType>(
  _documentNode: DocumentTypeDecoration<TType, any>,
  fragmentRef: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
): ReadonlyArray<TType>;
export function useFragment<TType>(
  _documentNode: DocumentTypeDecoration<TType, any>,
  fragmentRef: ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>> | null | undefined
): ReadonlyArray<TType> | null | undefined;

export function useFragment<TType>(
  _documentNode: DocumentTypeDecoration<TType, any>,
  fragmentRef:
    | FragmentType<DocumentTypeDecoration<TType, any>>
    | Array<FragmentType<DocumentTypeDecoration<TType, any>>>
    | ReadonlyArray<FragmentType<DocumentTypeDecoration<TType, any>>>
    | null
    | undefined
): TType | Array<TType> | ReadonlyArray<TType> | null | undefined {{
  return fragmentRef as any;
}}

// Optional: @defer support in TDN mode
type Incremental<T> = T | {{ [K in keyof T]?: never }};
export function isFragmentReady<TQuery, TFrag>(
  queryNode: DocumentTypeDecoration<TQuery, any>,
  fragmentNode: TypedDocumentNode<TFrag>,
  data: FragmentType<TypedDocumentNode<Incremental<TFrag>, any>> | null | undefined
): data is FragmentType<typeof fragmentNode> {{
  const deferredFields = (queryNode as {{ __meta__?: {{ deferredFields: Record<string, (keyof TFrag)[]> }} }})
    .__meta__?.deferredFields;
  if (!deferredFields) return true;

  const fragDef = fragmentNode.definitions[0] as FragmentDefinitionNode | undefined;
  const fragName = fragDef?.name?.value;
  const fields = (fragName && deferredFields[fragName]) || [];
  return fields.length > 0 && fields.every(field => data && field in data);
}}
`.trim()
                  
/** @type {{import('@graphql-codegen/cli').CodegenConfig}} */
const config = {{
    schema: '{schema_path}',
    documents: [
        '{fragments_dir}/*.graphql',
        '{mutations_dir}/*.graphql',
        '{queries_dir}/*.graphql',
    ],
    overwrite: true,
    generates: {{
        '{client_dir}/{app_name}.graphql.ts': {{
            plugins: [
                'add',
                'typescript',
                'typescript-operations',
                'typed-document-node',
            ],
            config: {{
                // add
                content: headerText,
                placement: 'prepend',
                // typescript
                avoidOptionals: true,
                declarationKind: 'interface',
                dedupeFragments: true,
                inputMaybeValue: 'T | null | undefined',
                maybeValue: 'T | null | undefined',
                nonOptionalTypename: true,
                useTypeImports: true,
                // typescript-operations
                dedupeOperationSuffix: true,
                inlineFragmentTypes: 'mask',
            }},
        }},
        '{client_dir}/fragment-masking.ts': {{
            plugins: [
                'add',
            ],
            config: {{
                content: fragmentMasking,
            }}
        }}
    }}
}}

module.exports = config;
""").lstrip()  # noqa: E501


def write_codegen_cjs(
    out_dir: Path,
    app_name: str,
    schema_path: Path,
    fragments_dir: Path,
    mutations_dir: Path,
    queries_dir: Path,
    client_dir: Path,
) -> Path:
    out_file = out_dir / "codegen.cjs"
    out_file.write_text(
        client_typescript_codegen_config(
            app_name=app_name,
            schema_path=schema_path,
            fragments_dir=fragments_dir,
            mutations_dir=mutations_dir,
            queries_dir=queries_dir,
            client_dir=client_dir,
        ),
        encoding="utf-8",
    )

    return out_file
