import { nanoid, prefixedId } from '@documenso/lib/universal/id';
import { prisma } from '@documenso/prisma';

import type { Field, Signature } from '@prisma/client';
import {
  DocumentSource,
  DocumentStatus,
  EnvelopeType,
  FieldType,
  Prisma,
  RecipientRole,
  SendStatus,
  SigningStatus,
} from '@prisma/client';

import { match } from 'ts-pattern';

import { DOCUMENT_AUDIT_LOG_TYPE, RECIPIENT_DIFF_TYPE } from '../../types/document-audit-logs';
import type { TRecipientActionAuthTypes } from '../../types/document-auth';
import { ZRecipientAuthOptionsSchema } from '../../types/document-auth';

import { ZFieldMetaSchema } from '../../types/field-meta';

import type { CreateDocumentAuditLogDataResponse } from '../../utils/document-audit-logs';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { createDocumentAuthOptions, createRecipientAuthOptions } from '../../utils/document-auth';

import type { incrementDocumentId } from '../envelope/increment-id';

import type { prepareDirectTemplate } from './prepare-direct-template';

type CreatedDirectRecipientField = {
  field: Field & { signature?: Signature | null };
  derivedRecipientActionAuth?: TRecipientActionAuthTypes;
};

export const createDirectTemplateEnvelope = async (
  context: Awaited<ReturnType<typeof prepareDirectTemplate>>,
  incrementedDocumentId: Awaited<ReturnType<typeof incrementDocumentId>>,
) => {
  const {
    directTemplateEnvelope,
    signatureLevel,
    directTemplateEnvelopeLegacyId,
    directTemplateExternalId,
    initialRequestTime,
    settings,
    envelopeItemsToCreate: preparedEnvelopeItems,
    templateAuthOptions,
    nonDirectTemplateRecipients,
    directTemplateRecipientAuthOptions,
    directTemplateRecipient,
    directTemplateNonSignatureFields,
    directTemplateSignatureFields,
    oldEnvelopeItemToNewEnvelopeItemIdMap,
    directRecipientEmail,
    directRecipientName,
    derivedRecipientAccessAuth,
    nextSigner,
    user,
    requestMetadata,
    derivedDocumentMeta,
  } = context;

  return await prisma.$transaction(async (tx) => {
    const documentMeta = await tx.documentMeta.create({ data: derivedDocumentMeta });
    const envelopeItemsToCreate = await Promise.all(
      preparedEnvelopeItems.map(async ({ documentData, ...item }) => {
        const createdData = await tx.documentData.create({
          data: { ...documentData, initialData: documentData.data },
        });
        return { ...item, documentDataId: createdData.id };
      }),
    );
    // Create the envelope and non direct template recipients.
    const createdEnvelope = await tx.envelope.create({
      data: {
        id: prefixedId('envelope'),
        secondaryId: incrementedDocumentId.formattedDocumentId,
        type: EnvelopeType.DOCUMENT,
        internalVersion: directTemplateEnvelope.internalVersion,
        signatureLevel,
        qrToken: prefixedId('qr'),
        source: DocumentSource.TEMPLATE_DIRECT_LINK,
        templateId: directTemplateEnvelopeLegacyId,
        userId: directTemplateEnvelope.userId,
        teamId: directTemplateEnvelope.teamId,
        title: directTemplateEnvelope.title,
        createdAt: initialRequestTime,
        status: DocumentStatus.PENDING,
        externalId: directTemplateExternalId,
        visibility: settings.documentVisibility,
        envelopeItems: {
          createMany: {
            data: envelopeItemsToCreate,
          },
        },
        authOptions: createDocumentAuthOptions({
          globalAccessAuth: templateAuthOptions.globalAccessAuth,
          globalActionAuth: templateAuthOptions.globalActionAuth,
        }),
        recipients: {
          createMany: {
            data: nonDirectTemplateRecipients.map((recipient) => {
              const authOptions = ZRecipientAuthOptionsSchema.parse(recipient?.authOptions);

              return {
                email: recipient.email,
                name: recipient.name,
                role: recipient.role,
                authOptions: createRecipientAuthOptions({
                  accessAuth: authOptions.accessAuth,
                  actionAuth: authOptions.actionAuth,
                }),
                sendStatus: recipient.role === RecipientRole.CC ? SendStatus.SENT : SendStatus.NOT_SENT,
                signingStatus: recipient.role === RecipientRole.CC ? SigningStatus.SIGNED : SigningStatus.NOT_SIGNED,
                signingOrder: recipient.signingOrder,
                token: nanoid(),
              };
            }),
          },
        },
        documentMetaId: documentMeta.id,
      },
      include: {
        recipients: true,
        team: {
          select: {
            url: true,
          },
        },
        envelopeItems: {
          select: {
            id: true,
          },
        },
      },
    });

    let nonDirectRecipientFieldsToCreate: Omit<Field, 'id' | 'secondaryId' | 'templateId'>[] = [];

    Object.values(nonDirectTemplateRecipients).forEach((templateRecipient) => {
      const recipient = createdEnvelope.recipients.find((recipient) => recipient.email === templateRecipient.email);

      if (!recipient) {
        throw new Error('Recipient not found.');
      }

      nonDirectRecipientFieldsToCreate = nonDirectRecipientFieldsToCreate.concat(
        templateRecipient.fields.map((field) => ({
          envelopeId: createdEnvelope.id,
          envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[field.envelopeItemId],
          recipientId: recipient.id,
          type: field.type,
          page: field.page,
          positionX: field.positionX,
          positionY: field.positionY,
          width: field.width,
          height: field.height,
          customText: '',
          inserted: false,
          fieldMeta: field.fieldMeta,
        })),
      );
    });

    await tx.field.createMany({
      data: nonDirectRecipientFieldsToCreate.map((field) => ({
        ...field,
        fieldMeta: field.fieldMeta ? ZFieldMetaSchema.parse(field.fieldMeta) : undefined,
      })),
    });

    // Create the direct recipient and their non signature fields.
    const createdDirectRecipient = await tx.recipient.create({
      data: {
        envelopeId: createdEnvelope.id,
        email: directRecipientEmail,
        name: directRecipientName,
        authOptions: createRecipientAuthOptions({
          accessAuth: directTemplateRecipientAuthOptions.accessAuth,
          actionAuth: directTemplateRecipientAuthOptions.actionAuth,
        }),
        role: directTemplateRecipient.role,
        token: nanoid(),
        signingStatus: SigningStatus.NOT_SIGNED,
        sendStatus: SendStatus.SENT,
        signedAt: null,
        signingOrder: directTemplateRecipient.signingOrder,
        fields: {
          createMany: {
            data: directTemplateNonSignatureFields.map(({ templateField, customText }) => {
              let inserted = true;

              // Custom logic for V2 to only insert if values exist.
              if (directTemplateEnvelope.internalVersion === 2) {
                inserted = customText !== '';
              }

              return {
                envelopeId: createdEnvelope.id,
                envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
                type: templateField.type,
                page: templateField.page,
                positionX: templateField.positionX,
                positionY: templateField.positionY,
                width: templateField.width,
                height: templateField.height,
                customText: customText ?? '',
                inserted,
                fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
              };
            }),
          },
        },
      },
      include: {
        fields: true,
      },
    });

    // Create any direct recipient signature fields.
    // Note: It's done like this because we can't nest things in createMany.
    const createdDirectRecipientSignatureFields: CreatedDirectRecipientField[] = await Promise.all(
      directTemplateSignatureFields.map(async ({ templateField, signature, derivedRecipientActionAuth }) => {
        if (!signature) {
          throw new Error('Not possible.');
        }

        const field = await tx.field.create({
          data: {
            envelopeId: createdEnvelope.id,
            envelopeItemId: oldEnvelopeItemToNewEnvelopeItemIdMap[templateField.envelopeItemId],
            recipientId: createdDirectRecipient.id,
            type: templateField.type,
            page: templateField.page,
            positionX: templateField.positionX,
            positionY: templateField.positionY,
            width: templateField.width,
            height: templateField.height,
            customText: '',
            inserted: true,
            fieldMeta: templateField.fieldMeta || Prisma.JsonNull,
            signature: {
              create: {
                recipientId: createdDirectRecipient.id,
                signatureImageAsBase64: signature.signatureImageAsBase64,
                typedSignature: signature.typedSignature,
              },
            },
          },
          include: {
            signature: true,
          },
        });

        return {
          field,
          derivedRecipientActionAuth,
        };
      }),
    );

    const createdDirectRecipientFields: CreatedDirectRecipientField[] = [
      ...createdDirectRecipient.fields.map((field) => ({
        field,
        derivedRecipientActionAuth: undefined,
      })),
      ...createdDirectRecipientSignatureFields,
    ];

    /**
     * Create the following audit logs.
     * - DOCUMENT_CREATED
     * - DOCUMENT_FIELD_INSERTED
     * - DOCUMENT_RECIPIENT_COMPLETED
     */
    const auditLogsToCreate: CreateDocumentAuditLogDataResponse[] = [
      createDocumentAuditLogData({
        type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_CREATED,
        envelopeId: createdEnvelope.id,
        user: {
          id: user?.id,
          name: user?.name,
          email: directRecipientEmail,
        },
        metadata: requestMetadata,
        data: {
          title: createdEnvelope.title,
          source: {
            type: DocumentSource.TEMPLATE_DIRECT_LINK,
            templateId: directTemplateEnvelopeLegacyId,
            directRecipientEmail,
          },
        },
      }),
      createDocumentAuditLogData({
        type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_OPENED,
        envelopeId: createdEnvelope.id,
        user: {
          id: user?.id,
          name: user?.name,
          email: directRecipientEmail,
        },
        metadata: requestMetadata,
        data: {
          recipientEmail: createdDirectRecipient.email,
          recipientId: createdDirectRecipient.id,
          recipientName: createdDirectRecipient.name,
          recipientRole: createdDirectRecipient.role,
          accessAuth: derivedRecipientAccessAuth || undefined,
        },
      }),
      ...createdDirectRecipientFields.map(({ field, derivedRecipientActionAuth }) =>
        createDocumentAuditLogData({
          type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_FIELD_INSERTED,
          envelopeId: createdEnvelope.id,
          user: {
            id: user?.id,
            name: user?.name,
            email: directRecipientEmail,
          },
          metadata: requestMetadata,
          data: {
            recipientEmail: createdDirectRecipient.email,
            recipientId: createdDirectRecipient.id,
            recipientName: createdDirectRecipient.name,
            recipientRole: createdDirectRecipient.role,
            fieldId: field.secondaryId,
            field: match(field.type)
              .with(FieldType.SIGNATURE, FieldType.FREE_SIGNATURE, (type) => ({
                type,
                data: field.signature?.signatureImageAsBase64 || field.signature?.typedSignature || '',
              }))
              .with(
                FieldType.DATE,
                FieldType.EMAIL,
                FieldType.INITIALS,
                FieldType.NAME,
                FieldType.TEXT,
                FieldType.NUMBER,
                FieldType.CHECKBOX,
                FieldType.DROPDOWN,
                FieldType.RADIO,
                (type) => ({
                  type,
                  data: field.customText,
                }),
              )
              .exhaustive(),
            fieldSecurity: derivedRecipientActionAuth
              ? {
                  type: derivedRecipientActionAuth,
                }
              : undefined,
          },
        }),
      ),
      createDocumentAuditLogData({
        type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_RECIPIENT_COMPLETED,
        envelopeId: createdEnvelope.id,
        user: {
          id: user?.id,
          name: user?.name,
          email: directRecipientEmail,
        },
        metadata: requestMetadata,
        data: {
          recipientEmail: createdDirectRecipient.email,
          recipientId: createdDirectRecipient.id,
          recipientName: createdDirectRecipient.name,
          recipientRole: createdDirectRecipient.role,
          actionAuth: createdDirectRecipient.authOptions?.actionAuth ?? [],
        },
      }),
    ];

    if (nextSigner) {
      const pendingRecipients = await tx.recipient.findMany({
        select: {
          id: true,
          signingOrder: true,
          name: true,
          email: true,
          role: true,
        },
        where: {
          envelopeId: createdEnvelope.id,
          id: { not: createdDirectRecipient.id },
          signingStatus: {
            not: SigningStatus.SIGNED,
          },
          role: {
            not: RecipientRole.CC,
          },
        },
        // Composite sort so our next recipient is always the one with the lowest signing order or id
        // if there is a tie.
        orderBy: [{ signingOrder: { sort: 'asc', nulls: 'last' } }, { id: 'asc' }],
      });

      const nextRecipient = pendingRecipients[0];

      if (nextRecipient) {
        auditLogsToCreate.push(
          createDocumentAuditLogData({
            type: DOCUMENT_AUDIT_LOG_TYPE.RECIPIENT_UPDATED,
            envelopeId: createdEnvelope.id,
            user: {
              name: user?.name || directRecipientName || '',
              email: user?.email || directRecipientEmail,
            },
            metadata: requestMetadata,
            data: {
              recipientEmail: nextRecipient.email,
              recipientName: nextRecipient.name,
              recipientId: nextRecipient.id,
              recipientRole: nextRecipient.role,
              changes: [
                {
                  type: RECIPIENT_DIFF_TYPE.NAME,
                  from: nextRecipient.name,
                  to: nextSigner.name,
                },
                {
                  type: RECIPIENT_DIFF_TYPE.EMAIL,
                  from: nextRecipient.email,
                  to: nextSigner.email,
                },
              ],
            },
          }),
        );

        await tx.recipient.update({
          where: { id: nextRecipient.id },
          data: {
            ...(nextSigner && documentMeta?.allowDictateNextSigner
              ? {
                  name: nextSigner.name,
                  email: nextSigner.email,
                }
              : {}),
          },
        });
      }
    }

    await tx.documentAuditLog.createMany({
      data: auditLogsToCreate,
    });

    const templateAttachments = await tx.envelopeAttachment.findMany({
      where: {
        envelopeId: directTemplateEnvelope.id,
      },
    });

    if (templateAttachments.length > 0) {
      await tx.envelopeAttachment.createMany({
        data: templateAttachments.map((attachment) => ({
          envelopeId: createdEnvelope.id,
          type: attachment.type,
          label: attachment.label,
          data: attachment.data,
        })),
      });
    }

    // Activate retention only after all evidence and attachments exist in this transaction.
    await tx.recipient.update({
      where: { id: createdDirectRecipient.id },
      data: { signingStatus: SigningStatus.SIGNED, signedAt: initialRequestTime },
    });

    return {
      createdEnvelope,
      documentMeta,
      token: createdDirectRecipient.token,
      recipientId: createdDirectRecipient.id,
    };
  });
};
