"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateModelCreateForm = generateModelCreateForm;
exports.generateModelUpdateForm = generateModelUpdateForm;
const generator_1 = require("@postxl/generator");
const utils_1 = require("@postxl/utils");
function generateModelCreateForm({ model, context }) {
    if (model.isReadonly) {
        throw new Error(`Cannot generate create form for readonly model ${model.name}`);
    }
    if (model.update.kind !== 'exists') {
        throw new Error(`Cannot generate create form for model ${model.name} without update actions`);
    }
    if (model.update.action.standardActions.create === undefined) {
        throw new Error(`Cannot generate create form for model ${model.name} without create action`);
    }
    const imports = generator_1.ImportGenerator
        //
        .from(model.forms.create.location)
        .add(model.itemsHook)
        .addImports({
        [(0, generator_1.toPackageName)('@tanstack/react-form')]: [(0, generator_1.toFunctionName)('useForm')],
        [(0, generator_1.toPackageName)('react')]: [(0, generator_1.toFunctionName)('Fragment'), (0, generator_1.toFunctionName)('useCallback')],
        [(0, generator_1.toBackendModuleLocation)('@components/ui/button/button')]: [(0, generator_1.toClassName)('Button')],
        [(0, generator_1.toBackendModuleLocation)('@components/ui/dialog/dialog')]: [
            (0, generator_1.toClassName)('Dialog'),
            (0, generator_1.toClassName)('DialogClose'),
            (0, generator_1.toClassName)('DialogContent'),
            (0, generator_1.toClassName)('DialogDescription'),
            (0, generator_1.toClassName)('DialogFooter'),
            (0, generator_1.toClassName)('DialogHeader'),
            (0, generator_1.toClassName)('DialogTitle'),
        ],
        [(0, generator_1.toBackendModuleLocation)('@components/ui/field/field')]: [
            (0, generator_1.toClassName)('Field'),
            (0, generator_1.toClassName)('FieldError'),
            (0, generator_1.toClassName)('FieldGroup'),
            (0, generator_1.toClassName)('FieldLabel'),
        ],
    });
    const inputType = `${model.types.name}CreateInput`;
    const outputType = `${model.types.name}Create`;
    // Extract all the interesting editable fields for this form. We obviously want
    // to skip the model's `id` field because that's generated by the database, as
    // well as any readonly fields as they cannot be set by the user.
    const fields = Array.from(model.fields.values())
        .filter((field) => field.kind !== 'id' && !field.isReadonly)
        .map((field) => generateModelFormField({ context, imports, model, field, isCreate: true }));
    return `
  ${imports.generate()}
  import * as z from "zod"

  export type ${model.forms.create.name}Props = Readonly<{
    /**
     * When true, the form includes a \`DialogFooter\` with a button to close the dialog.
     */
    modal?: boolean

    /** 
     * Callback invoked when a ${model.userFriendlyName} has been created, 
     * user clicked submit and input is valid.
     * If this is left undefined, the default create mutation will be used instead. 
     */
    onSubmit?: (item: ${outputType}) => void
    
    /**
     * Callback invoked when a ${model.userFriendlyName} has been successfully created.
     */
    onSuccess?: (item: ${outputType}) => void
    
    /**
     * Callback invoked when an error occurred during creation of a ${model.userFriendlyName}.
     */
    onError?: (item: ${outputType}, error: unknown) => void
  }>

  const schema = z.object({
    ${fields.map((field) => `${field.key}: ${field.schema}`).join(',\n    ')}
  })

  export type ${inputType} = z.input<typeof schema>
  export type ${outputType} = z.infer<typeof schema>

  const defaultValues = {
    ${fields.map((field) => `${field.key}: ${field.defaultValue}`).join(',\n    ')}
  } as ${inputType}

  export function ${model.forms.create.name}({ modal, onSubmit, onSuccess, onError }: ${model.forms.create.name}Props) {
    const { ${model.itemsHook.create.name}: defaultCreate } = ${model.itemsHook.name}()
    const onFormSubmit = useCallback(
      async ({ value }: { value: ${inputType} }) => {
        const data: ${outputType} = value as ${outputType}  
        try {
          await (onSubmit ?? defaultCreate)(data)
          onSuccess?.(data)
        } catch (error) {
          onError?.(data, error)
        }
      },
      [defaultCreate, onError, onSubmit, onSuccess],
    )
    
    const form = useForm({
      defaultValues,
      validators: {
        onChange: schema
      },
      onSubmit: onFormSubmit
    })

    const handleSubmit = (event: React.FormEvent) => {
      event.preventDefault()
      event.stopPropagation()

      void form.handleSubmit() // NOSONAR
    }

    const Footer = modal ? DialogFooter : Fragment

    return (
      <form onSubmit={handleSubmit}>
        <FieldGroup>
          ${fields
        .filter((field) => field.field.kind !== 'id' && field.formField !== null)
        .map((field) => field.formField)
        .join('\n')}
        </FieldGroup>

        <Footer>
          <Field orientation="horizontal">
            {modal && (
              <DialogClose asChild>
                <Button type="button" variant="outline">
                  Cancel
                </Button>
              </DialogClose>
            )}

            <Button type="button" variant="outline" onClick={() => form.reset()}>
              Reset
            </Button>

            <Button type="submit">
              Submit
            </Button>
          </Field>
        </Footer>
      </form>
    )
  }

  export type ${model.forms.createModal.name}Props = Readonly<{
    open?: boolean
    children?: React.ReactNode
    onClose?: () => void
  } & Omit<${model.forms.create.name}Props, 'modal'>>

  export function ${model.forms.createModal.name}({ open, children, onClose, ...props }: ${model.forms.createModal.name}Props) {
    return (
      <Dialog open={open} onOpenChange={onClose}>
        {children}

        <DialogContent>
          <DialogHeader>
            <DialogTitle>Create ${model.userFriendlyName}</DialogTitle>
            <DialogDescription>
              Fill in the form below to create a new ${model.userFriendlyName}.
            </DialogDescription>
          </DialogHeader>

          <${model.forms.create.name} modal {...props} />
        </DialogContent>
      </Dialog>
    )
  }
  `;
}
function generateModelUpdateForm({ model, context }) {
    if (model.isReadonly) {
        throw new Error(`Cannot generate update form for readonly model ${model.name}`);
    }
    if (model.update.kind !== 'exists') {
        throw new Error(`Cannot generate update form for model ${model.name} without update actions`);
    }
    if (model.update.action.standardActions.update === undefined) {
        throw new Error(`Cannot generate update form for model ${model.name} without update action`);
    }
    const imports = generator_1.ImportGenerator
        //
        .from(model.forms.update.location)
        .addType(model.types)
        .add(model.itemsHook)
        .add(model.types.id.decoder)
        .addImports({
        [(0, generator_1.toPackageName)('@tanstack/react-form')]: [(0, generator_1.toFunctionName)('useForm')],
        [(0, generator_1.toPackageName)('react')]: [(0, generator_1.toFunctionName)('Fragment'), (0, generator_1.toFunctionName)('useCallback')],
        [(0, generator_1.toBackendModuleLocation)('@components/ui/button/button')]: [(0, generator_1.toClassName)('Button')],
        [(0, generator_1.toBackendModuleLocation)('@components/ui/dialog/dialog')]: [
            (0, generator_1.toClassName)('Dialog'),
            (0, generator_1.toClassName)('DialogClose'),
            (0, generator_1.toClassName)('DialogContent'),
            (0, generator_1.toClassName)('DialogDescription'),
            (0, generator_1.toClassName)('DialogFooter'),
            (0, generator_1.toClassName)('DialogHeader'),
            (0, generator_1.toClassName)('DialogTitle'),
        ],
        [(0, generator_1.toBackendModuleLocation)('@components/ui/field/field')]: [
            (0, generator_1.toClassName)('Field'),
            (0, generator_1.toClassName)('FieldError'),
            (0, generator_1.toClassName)('FieldGroup'),
            (0, generator_1.toClassName)('FieldLabel'),
        ],
    });
    const inputType = `${model.types.name}UpdateInput`;
    const outputType = `${model.types.name}Update`;
    // Extract all the interesting editable fields for this form. We obviously want
    // to skip the model's `id` field because that's generated by the database, as
    // well as any readonly fields as they cannot be set by the user.
    const fields = Array.from(model.fields.values())
        .filter((field) => !field.isReadonly)
        .map((field) => generateModelFormField({ context, imports, model, field, isCreate: false }));
    return `
  ${imports.generate()}
  import * as z from "zod"

  export type ${model.forms.update.name}Props = Readonly<{
    initial: ${model.types.name}

    /**
     * When true, the form includes a \`DialogFooter\` with a button to close the dialog.
     */
    modal?: boolean

    /** 
     * Callback invoked when a ${model.userFriendlyName} has been updated, 
     * user clicked submit and input is valid.
     * If this is left undefined, the default update mutation will be used instead. 
     */
    onSubmit?: (item: ${outputType}) => void
    
    /**
     * Callback invoked when a ${model.userFriendlyName} has been successfully updated.
     */
    onSuccess?: (item: ${outputType}) => void
    
    /**
     * Callback invoked when an error occurred during update of a ${model.userFriendlyName}.
     */
    onError?: (item: ${outputType}, error: unknown) => void
  }>

  const schema = z.object({
    ${fields.map((field) => `${field.key}: ${field.schema}`).join(',\n    ')}
  })

  export type ${inputType} = z.input<typeof schema>
  export type ${outputType} = z.infer<typeof schema>

  export function ${model.forms.update.name}({ initial, modal, onSubmit, onSuccess, onError }: ${model.forms.update.name}Props) {
    const { ${model.itemsHook.update.name}: defaultUpdate } = ${model.itemsHook.name}()
    const onFormSubmit = useCallback(
      async ({ value }: { value: ${inputType} }) => {
        const data: ${outputType} = { ...initial, ...(value as ${outputType}) }
        try {
          await (onSubmit ?? defaultUpdate)(data)
          onSuccess?.(data)
        } catch (error) {
          onError?.(data, error)
        }
      },
      [defaultUpdate, initial, onError, onSubmit, onSuccess],
    )  

    const form = useForm({
      defaultValues: {
        ${fields.map((field) => `${field.key}: initial.${field.key}`).join(',\n    ')}
      } as ${inputType},
      validators: {
        onChange: schema
      },
      onSubmit: onFormSubmit
    })

    const handleSubmit = (event: React.FormEvent) => {
      event.preventDefault()
      event.stopPropagation()

      void form.handleSubmit() // NOSONAR
    }

    const Footer = modal ? DialogFooter : Fragment

    return (
      <form onSubmit={handleSubmit}>
        <FieldGroup>
          ${fields
        .filter((field) => field.field.kind !== 'id' && field.formField !== null)
        .map((field) => field.formField)
        .join('\n')}
        </FieldGroup>

        <Footer>
          <Field orientation="horizontal">
            {modal && (
              <DialogClose asChild>
                <Button type="button" variant="outline">
                  Cancel
                </Button>
              </DialogClose>
            )}

            <Button type="button" variant="outline" onClick={() => form.reset()}>
              Reset
            </Button>

            <Button type="submit">
              Submit
            </Button>
          </Field>
        </Footer>
      </form>
    )
  }

  export type ${model.forms.updateModal.name}Props = {
    open?: boolean
    children?: React.ReactNode
    onClose?: () => void
  } & Omit<${model.forms.update.name}Props, 'modal'>

  export function ${model.forms.updateModal.name}({ open, children, onClose, ...props }: ${model.forms.updateModal.name}Props) {
    return (
      <Dialog open={open} onOpenChange={onClose}>
        {children}

        <DialogContent>
          <DialogHeader>
            <DialogTitle>Update ${model.userFriendlyName}</DialogTitle>
            <DialogDescription>
              Fill in the form below to update an existing ${model.userFriendlyName}.
            </DialogDescription>
          </DialogHeader>

          <${model.forms.update.name} modal {...props} />
        </DialogContent>
      </Dialog>
    )
  }
  `;
}
function generateFormFieldCode(field, input) {
    return `
    <form.Field name="${field.name}">
      {(field) => {
        const isInvalid = field.state.meta.isTouched && !field.state.meta.isValid

        return (
          <Field data-invalid={isInvalid}>
            <FieldLabel htmlFor={field.name}>
              ${field.label}
            </FieldLabel>

            ${input}

            {isInvalid && <FieldError errors={field.state.meta.errors} />}
          </Field>
        )
      }}
    </form.Field>
  `;
}
function generateModelFormField({ context, imports, model, field, isCreate, }) {
    switch (field.kind) {
        case 'id':
            return {
                field,
                key: field.name,
                schema: `${model.types.id.decoder.name}`,
                defaultValue: `''`,
                formField: null,
            };
        case 'scalar':
            return generateModelFormField_Scalar({ imports, field });
        case 'enum':
            return generateModelFormField_Enum({ imports, field, context });
        case 'relation':
            return generateModelFormField_Relation({ imports, field, context });
        case 'discriminatedUnion':
            return generateModelFormField_DiscriminatedUnion({ imports, field, context, isCreate });
        default:
            throw new utils_1.ExhaustiveSwitchCheck(field);
    }
}
function generateModelFormField_Scalar({ imports, field, }) {
    switch (field.type) {
        case 'string':
            imports.addImport({
                from: (0, generator_1.toBackendModuleLocation)('@components/ui/input/input'),
                items: [(0, generator_1.toClassName)('Input')],
            });
            return {
                field,
                key: field.name,
                schema: `z.string()${field.isRequired ? '' : '.nullable()'}`,
                defaultValue: `''`,
                formField: generateFormFieldCode(field, `
            <Input
              id={field.name}
              name={field.name}
              value={${field.isRequired ? 'field.state.value' : 'field.state.value ?? ""'}}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.value)}
              aria-invalid={isInvalid}
              autoComplete="off"
            />
          `),
            };
        case 'number':
            imports.addImport({
                from: (0, generator_1.toBackendModuleLocation)('@components/ui/input/input'),
                items: [(0, generator_1.toClassName)('Input')],
            });
            return {
                field,
                key: field.name,
                schema: `z.number()${field.isRequired ? '' : '.nullable()'}`,
                defaultValue: `0`,
                formField: generateFormFieldCode(field, `
            <Input
              type="number"
              id={field.name}
              name={field.name}
              value={${field.isRequired ? 'field.state.value' : 'field.state.value ?? ""'}}
              onBlur={field.handleBlur}
              onChange={(e) => field.handleChange(e.target.valueAsNumber)}
              aria-invalid={isInvalid}
              autoComplete="off"
            />
          `),
            };
        case 'boolean':
            imports.addImport({
                from: (0, generator_1.toBackendModuleLocation)('@components/ui/checkbox/shadcn-checkbox'),
                items: [(0, generator_1.toClassName)('Checkbox')],
            });
            return {
                field,
                key: field.name,
                schema: `z.boolean()${field.isRequired ? '' : '.nullable()'}`,
                defaultValue: `false`,
                formField: generateFormFieldCode(field, `
            <Checkbox
              id={field.name}
              name={field.name}
              checked={${field.isRequired ? 'field.state.value' : '!!field.state.value'}}
              onBlur={field.handleBlur}
              onCheckedChange={(checked) => field.handleChange(!!checked)}
              aria-invalid={isInvalid}
            />
          `),
            };
        case 'Date':
            return {
                field,
                key: field.name,
                schema: `z.date()${field.isRequired ? '' : '.nullable()'}`,
                defaultValue: `${field.isRequired ? 'new Date()' : 'null'}`,
                formField: generateFormFieldCode(field, `
            Not implemented yet
          `),
            };
        default:
            throw new utils_1.ExhaustiveSwitchCheck(field);
    }
}
function generateModelFormField_Enum({ imports, field, context, }) {
    const enum_ = context.enums.get(field.enumName);
    imports.add(enum_.inputs.select);
    return {
        field,
        key: field.name,
        schema: `z.enum([${Array.from(enum_.members.values())
            .map((v) => `'${v.value}'`)
            .join(', ')}])${field.isRequired ? '' : '.nullable()'}`,
        defaultValue: field.isRequired ? `'${Array.from(enum_.members.values())[0]?.value ?? 'null'}'` : `null`,
        formField: generateFormFieldCode(field, `
        <${enum_.inputs.select.name}
          name={field.name}
          value={field.state.value}
          onValueChange={(value) => {
            field.handleBlur()
            field.handleChange(value)}
          }
          aria-invalid={isInvalid}
        />
        `),
    };
}
function generateModelFormField_Relation({ imports, field, context, }) {
    const relatedModel = context.models.get(field.referencedModelName);
    imports.add(relatedModel.forms.inputs.select).add(relatedModel.types.id.toBranded).add(relatedModel.types.id.decoder);
    return {
        field,
        key: field.name,
        schema: `${relatedModel.types.id.decoder.name}${field.isRequired ? '' : '.nullable()'}`,
        defaultValue: field.isRequired ? `''` : `null`,
        formField: generateFormFieldCode(field, `
        <${relatedModel.forms.inputs.select.name}
          name={field.name}
          value={${field.isRequired
            ? `${relatedModel.types.id.toBranded.name}(field.state.value)`
            : `field.state.value === null ? null : ${relatedModel.types.id.toBranded.name}(field.state.value)`}}
          label={(item) => item.${relatedModel.labelField.name}}
          onValueChange={(value) => {
            field.handleBlur()
            field.handleChange(value.id)}
          }
          aria-invalid={isInvalid}
        />
        `),
    };
}
function generateModelFormField_DiscriminatedUnion({ imports, field, context, isCreate, }) {
    const model = context.models.get(field.modelName);
    imports
        .add(model.forms.discriminatedUnions.get(field.name).field)
        .add(model.forms.discriminatedUnions.get(field.name).schema);
    if (isCreate) {
        imports.add(model.forms.discriminatedUnions.get(field.name).defaults);
    }
    return {
        field,
        key: field.name,
        schema: `${model.forms.discriminatedUnions.get(field.name).schema.name}${field.isRequired ? '' : '.nullable()'}`,
        defaultValue: field.isRequired ? `${model.forms.discriminatedUnions.get(field.name).defaults.name}` : `null`,
        formField: generateFormFieldCode(field, `
        <${model.forms.discriminatedUnions.get(field.name).field.name}
        value ={field.state.value}
        onChange={(value) => {
          field.handleBlur()
          field.handleChange(value)
        }}
        label={field.name}
        aria-invalid={isInvalid}
        />
      `),
    };
}
//# sourceMappingURL=forms.generator.js.map