import { validateCheckboxField } from '@documenso/lib/advanced-fields-validation/validate-checkbox';
import { validateDropdownField } from '@documenso/lib/advanced-fields-validation/validate-dropdown';
import { validateNumberField } from '@documenso/lib/advanced-fields-validation/validate-number';
import { validateRadioField } from '@documenso/lib/advanced-fields-validation/validate-radio';
import { validateTextField } from '@documenso/lib/advanced-fields-validation/validate-text';
import {
  FIELD_META_DEFAULT_VALUES,
  type TFieldMetaSchema as FieldMeta,
  ZCheckboxFieldMeta,
  ZDropdownFieldMeta,
  ZFieldMetaSchema,
  ZNumberFieldMeta,
  ZRadioFieldMeta,
  ZTextFieldMeta,
} from '@documenso/lib/types/field-meta';
import { prisma } from '@documenso/prisma';
import { EnvelopeType, FieldType } from '@prisma/client';

import { AppError, AppErrorCode } from '../../errors/app-error';
import type { EnvelopeIdOptions } from '../../utils/envelope';
import { mapFieldToLegacyField } from '../../utils/fields';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';

export type SetFieldsForTemplateOptions = {
  userId: number;
  teamId: number;
  id: EnvelopeIdOptions;
  fields: {
    id?: number | null;
    formId?: string;
    envelopeItemId: string;
    type: FieldType;
    recipientId: number;
    pageNumber: number;
    pageX: number;
    pageY: number;
    pageWidth: number;
    pageHeight: number;
    fieldMeta?: FieldMeta;
  }[];
};

export const setFieldsForTemplate = async ({ userId, teamId, id, fields }: SetFieldsForTemplateOptions) => {
  const { envelopeWhereInput } = await getEnvelopeWhereInput({
    id,
    type: EnvelopeType.TEMPLATE,
    userId,
    teamId,
  });

  const envelope = await prisma.envelope.findFirst({
    where: envelopeWhereInput,
    include: {
      recipients: true,
      envelopeItems: {
        select: {
          id: true,
        },
      },
      fields: {
        include: {
          recipient: true,
        },
      },
    },
  });

  if (!envelope) {
    throw new AppError(AppErrorCode.NOT_FOUND, {
      message: 'Document not found',
    });
  }

  const existingFields = envelope.fields;

  const removedFields = existingFields.filter(
    (existingField) => !fields.find((field) => field.id === existingField.id),
  );

  const linkedFields = fields.map((field) => {
    const existing = existingFields.find((existingField) => existingField.id === field.id);

    const recipient = envelope.recipients.find((recipient) => recipient.id === field.recipientId);

    // Check whether the field is being attached to an allowed envelope item.
    const foundEnvelopeItem = envelope.envelopeItems.find((envelopeItem) => envelopeItem.id === field.envelopeItemId);

    if (!foundEnvelopeItem) {
      throw new AppError(AppErrorCode.INVALID_REQUEST, {
        message: `Envelope item ${field.envelopeItemId} not found`,
      });
    }

    // Each field MUST have a recipient associated with it.
    if (!recipient) {
      throw new AppError(AppErrorCode.INVALID_REQUEST, {
        message: `Recipient not found for field ${field.id}`,
      });
    }

    return {
      ...field,
      _persisted: existing,
      _recipient: recipient,
    };
  });

  const persistedFields = await Promise.all(
    // Disabling as wrapping promises here causes type issues
    // eslint-disable-next-line @typescript-eslint/promise-function-async
    linkedFields.map(async (field) => {
      const parsedFieldMeta = field.fieldMeta
        ? ZFieldMetaSchema.parse(field.fieldMeta)
        : FIELD_META_DEFAULT_VALUES[field.type];

      if (field.type === FieldType.TEXT && field.fieldMeta) {
        const textFieldParsedMeta = ZTextFieldMeta.parse(field.fieldMeta);
        const errors = validateTextField(textFieldParsedMeta.text || '', textFieldParsedMeta);
        if (errors.length > 0) {
          throw new Error(errors.join(', '));
        }
      }

      if (field.type === FieldType.NUMBER && field.fieldMeta) {
        const numberFieldParsedMeta = ZNumberFieldMeta.parse(field.fieldMeta);
        const errors = validateNumberField(String(numberFieldParsedMeta.value || ''), numberFieldParsedMeta);
        if (errors.length > 0) {
          throw new Error(errors.join(', '));
        }
      }

      if (field.type === FieldType.CHECKBOX) {
        if (!field.fieldMeta) {
          throw new Error('Checkbox field is missing required metadata');
        }
        const checkboxFieldParsedMeta = ZCheckboxFieldMeta.parse(field.fieldMeta);
        const errors = validateCheckboxField(
          checkboxFieldParsedMeta?.values?.map((item) => item.value) ?? [],
          checkboxFieldParsedMeta,
        );
        if (errors.length > 0) {
          throw new Error(errors.join(', '));
        }
      }

      if (field.type === FieldType.RADIO) {
        if (!field.fieldMeta) {
          throw new Error('Radio field is missing required metadata');
        }
        const radioFieldParsedMeta = ZRadioFieldMeta.parse(field.fieldMeta);
        const checkedRadioFieldValue = radioFieldParsedMeta.values?.find((option) => option.checked)?.value;
        const errors = validateRadioField(checkedRadioFieldValue, radioFieldParsedMeta);
        if (errors.length > 0) {
          throw new Error(errors.join('. '));
        }
      }

      if (field.type === FieldType.DROPDOWN) {
        if (!field.fieldMeta) {
          throw new Error('Dropdown field is missing required metadata');
        }
        const dropdownFieldParsedMeta = ZDropdownFieldMeta.parse(field.fieldMeta);
        const errors = validateDropdownField(undefined, dropdownFieldParsedMeta);
        if (errors.length > 0) {
          throw new Error(errors.join('. '));
        }
      }

      // Proceed with upsert operation
      const upsertedField = await prisma.field.upsert({
        where: {
          id: field._persisted?.id ?? -1,
          envelopeId: envelope.id,
          envelopeItemId: field.envelopeItemId,
        },
        update: {
          page: field.pageNumber,
          positionX: field.pageX,
          positionY: field.pageY,
          width: field.pageWidth,
          height: field.pageHeight,
          fieldMeta: parsedFieldMeta,
        },
        create: {
          type: field.type,
          page: field.pageNumber,
          positionX: field.pageX,
          positionY: field.pageY,
          width: field.pageWidth,
          height: field.pageHeight,
          customText: '',
          inserted: false,
          fieldMeta: parsedFieldMeta,
          envelope: {
            connect: {
              id: envelope.id,
            },
          },
          envelopeItem: {
            connect: {
              id: field.envelopeItemId,
              envelopeId: envelope.id,
            },
          },
          recipient: {
            connect: {
              id: field._recipient.id,
              envelopeId: envelope.id,
            },
          },
        },
      });

      return {
        ...upsertedField,
        formId: field.formId,
      };
    }),
  );

  if (removedFields.length > 0) {
    await prisma.field.deleteMany({
      where: {
        id: {
          in: removedFields.map((field) => field.id),
        },
      },
    });
  }

  // Filter out fields that have been removed or have been updated.
  const filteredFields = existingFields.filter((field) => {
    const isRemoved = removedFields.find((removedField) => removedField.id === field.id);
    const isUpdated = persistedFields.find((persistedField) => persistedField.id === field.id);

    return !isRemoved && !isUpdated;
  });

  const mappedFilteredFields = filteredFields.map((field) => ({
    ...mapFieldToLegacyField(field, envelope),
    formId: undefined,
  }));

  const mappedPersistentFields = persistedFields.map((field) => ({
    ...mapFieldToLegacyField(field, envelope),
    formId: field?.formId,
  }));

  return {
    fields: [...mappedFilteredFields, ...mappedPersistentFields],
  };
};
