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

import { DocumentSigningOrder, FieldType, RecipientRole } from '@prisma/client';
import { DateTime } from 'luxon';
import { match } from 'ts-pattern';

import { AppError, AppErrorCode } from '../../errors/app-error';

import { DocumentAccessAuth, ZRecipientAuthOptionsSchema } from '../../types/document-auth';

import { ZSignatureLevelSchema } from '../../types/signature-level';

import { getFileServerSide } from '../../universal/upload/get-file.server';
import { preparePdfFileServerSide } from '../../universal/upload/put-file.server';
import { isRequiredField } from '../../utils/advanced-fields-helpers';
import { extractDerivedDocumentMeta } from '../../utils/document';

import { extractDocumentAuthMethods } from '../../utils/document-auth';
import { mapSecondaryIdToTemplateId } from '../../utils/envelope';
import { getRecipientsWithMissingFields } from '../../utils/recipients';

import { validateFieldAuth } from '../document/validate-field-auth';

import { assertOrganisationRatesAndLimits } from '../rate-limit/assert-organisation-rates-and-limits';
import { resolveSignatureLevel } from '../signature-level/resolve-signature-level';
import { getTeamSettings } from '../team/get-team-settings';

import type { CreateDocumentFromDirectTemplateOptions } from './direct-template-types';

export const prepareDirectTemplate = async ({
  directRecipientName: initialDirectRecipientName,
  directRecipientEmail,
  directTemplateToken,
  directTemplateExternalId,
  signedFieldValues,
  templateUpdatedAt,
  nextSigner,
  requestMetadata,
  user,
}: CreateDocumentFromDirectTemplateOptions) => {
  const directTemplateEnvelope = await prisma.envelope.findFirst({
    where: {
      directLink: {
        token: directTemplateToken,
      },
    },
    include: {
      recipients: {
        include: {
          fields: true,
        },
      },
      directLink: true,
      envelopeItems: {
        include: {
          documentData: true,
        },
      },
      documentMeta: true,
      user: {
        select: {
          id: true,
          email: true,
          name: true,
        },
      },
      team: {
        select: {
          organisationId: true,
          organisation: {
            select: {
              organisationClaim: {
                select: {
                  recipientCount: true,
                },
              },
            },
          },
        },
      },
    },
  });

  if (!directTemplateEnvelope?.directLink?.enabled) {
    throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Invalid or missing template' });
  }

  if (
    nextSigner &&
    (!directTemplateEnvelope.documentMeta?.allowDictateNextSigner ||
      directTemplateEnvelope.documentMeta?.signingOrder !== DocumentSigningOrder.SEQUENTIAL)
  ) {
    throw new AppError(AppErrorCode.INVALID_REQUEST, {
      message: 'You need to enable allowDictateNextSigner and sequential signing to dictate the next signer',
    });
  }

  const directTemplateEnvelopeLegacyId = mapSecondaryIdToTemplateId(directTemplateEnvelope.secondaryId);

  if (directTemplateEnvelope.envelopeItems.length < 1) {
    throw new AppError(AppErrorCode.INVALID_REQUEST, {
      message: 'Invalid number of envelope items',
    });
  }

  const settings = await getTeamSettings({
    userId: directTemplateEnvelope.userId,
    teamId: directTemplateEnvelope.teamId,
  });

  const { recipients, directLink } = directTemplateEnvelope;

  const directTemplateRecipient = recipients.find((recipient) => recipient.id === directLink.directTemplateRecipientId);

  if (!directTemplateRecipient || directTemplateRecipient.role === RecipientRole.CC) {
    throw new AppError(AppErrorCode.INVALID_REQUEST, {
      message: 'Invalid or missing direct recipient',
    });
  }

  const recipientsWithMissingFields = getRecipientsWithMissingFields(
    recipients,
    recipients.flatMap((recipient) => recipient.fields),
  );

  if (recipientsWithMissingFields.length > 0) {
    throw new AppError(AppErrorCode.MISSING_SIGNATURE_FIELD, {
      message: 'One or more signers on this direct template are missing a signature field',
    });
  }

  if (directTemplateEnvelope.updatedAt.getTime() !== templateUpdatedAt.getTime()) {
    throw new AppError(AppErrorCode.INVALID_REQUEST, { message: 'Template no longer matches' });
  }

  const { derivedRecipientAccessAuth, documentAuthOption: templateAuthOptions } = extractDocumentAuthMethods({
    documentAuth: directTemplateEnvelope.authOptions,
  });

  let directRecipientName = user?.name || initialDirectRecipientName;

  // Ensure typesafety when we add more options.
  const isAccessAuthValid = match(derivedRecipientAccessAuth.at(0))
    .with(DocumentAccessAuth.ACCOUNT, () => user && user?.email === directRecipientEmail)
    .with(DocumentAccessAuth.TWO_FACTOR_AUTH, () => false) // Not supported for direct templates
    .with(undefined, () => true)
    .exhaustive();

  if (!isAccessAuthValid) {
    throw new AppError(AppErrorCode.UNAUTHORIZED, { message: 'You must be logged in' });
  }

  const directTemplateRecipientAuthOptions = ZRecipientAuthOptionsSchema.parse(directTemplateRecipient.authOptions);

  const nonDirectTemplateRecipients = directTemplateEnvelope.recipients.filter(
    (recipient) => recipient.id !== directTemplateRecipient.id,
  );

  // Carry the template's level forward, coercing if the instance mode has
  // changed since the template was created. ZSignatureLevelSchema parses the
  // free-form TEXT column defensively. Resolved before meta extraction so
  // signingOrder picks up the TSP-appropriate default + assertion.
  const signatureLevel = resolveSignatureLevel({
    requested: ZSignatureLevelSchema.parse(directTemplateEnvelope.signatureLevel),
    strict: false,
  });

  const derivedDocumentMeta = extractDerivedDocumentMeta(settings, directTemplateEnvelope.documentMeta, signatureLevel);

  // The resulting document contains every non-direct template recipient plus the
  // direct recipient that is signing now. A recipientCount of 0 means unlimited.
  // This mirrors the check in `sendDocument`, but must be done here because this
  // flow creates the document directly in PENDING and swallows `sendDocument` errors.
  const maximumRecipientCount = directTemplateEnvelope.team.organisation.organisationClaim.recipientCount;
  const resultingRecipientCount = nonDirectTemplateRecipients.length + 1;

  if (maximumRecipientCount > 0 && resultingRecipientCount > maximumRecipientCount) {
    throw new AppError('RECIPIENT_LIMIT_EXCEEDED', {
      message: `You cannot send a document with more than ${maximumRecipientCount} recipients`,
      statusCode: 400,
    });
  }

  // Associate, validate and map to a query every direct template recipient field with the provided fields.
  // Only process fields that are either required or have been signed by the user
  const fieldsToProcess = directTemplateRecipient.fields.filter((templateField) => {
    const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);

    // Custom logic for V2 to include all fields, since v1 excludes read only
    // and prefilled fields.
    if (directTemplateEnvelope.internalVersion === 2) {
      return true;
    }

    // Include if it's required or has a signed value
    return isRequiredField(templateField) || signedFieldValue !== undefined;
  });

  const createDirectRecipientFieldArgs = await Promise.all(
    fieldsToProcess.map(async (templateField) => {
      const signedFieldValue = signedFieldValues.find((value) => value.fieldId === templateField.id);

      if (isRequiredField(templateField) && !signedFieldValue) {
        throw new AppError(AppErrorCode.INVALID_BODY, {
          message: 'Invalid, missing or changed fields',
        });
      }

      if (templateField.type === FieldType.NAME && directRecipientName === undefined) {
        directRecipientName = signedFieldValue?.value;
      }

      const derivedRecipientActionAuth = await validateFieldAuth({
        documentAuthOptions: directTemplateEnvelope.authOptions,
        recipient: {
          authOptions: directTemplateRecipient.authOptions,
          email: directRecipientEmail,
          envelopeId: directTemplateEnvelope.id,
        },
        field: templateField,
        userId: user?.id,
        authOptions: signedFieldValue?.authOptions,
      });

      if (!signedFieldValue) {
        return {
          templateField,
          customText: '',
          derivedRecipientActionAuth,
          signature: null,
        };
      }

      const { value, isBase64 } = signedFieldValue;

      const isSignatureField =
        templateField.type === FieldType.SIGNATURE || templateField.type === FieldType.FREE_SIGNATURE;

      let customText = !isSignatureField ? value : '';

      const signatureImageAsBase64 = isSignatureField && isBase64 ? value : undefined;
      const typedSignature = isSignatureField && !isBase64 ? value : undefined;

      if (templateField.type === FieldType.DATE) {
        customText = DateTime.now().setZone(derivedDocumentMeta.timezone).toFormat(derivedDocumentMeta.dateFormat);
      }

      if (isSignatureField && !signatureImageAsBase64 && !typedSignature) {
        throw new Error('Signature field must have a signature');
      }

      return {
        templateField,
        customText,
        derivedRecipientActionAuth,
        signature: isSignatureField
          ? {
              signatureImageAsBase64,
              typedSignature,
            }
          : null,
      };
    }),
  );

  const directTemplateNonSignatureFields = createDirectRecipientFieldArgs.filter(({ signature }) => signature === null);

  const directTemplateSignatureFields = createDirectRecipientFieldArgs.filter(({ signature }) => signature !== null);

  // Enforce the organisation document-creation limit before creating the document.
  await assertOrganisationRatesAndLimits({
    organisationId: directTemplateEnvelope.team.organisationId,
    type: 'document',
    count: 1,
  });

  const initialRequestTime = new Date();

  // Key = original envelope item ID
  // Value = duplicated envelope item ID.
  const oldEnvelopeItemToNewEnvelopeItemIdMap: Record<string, string> = {};

  // Duplicate the envelope item data.
  const envelopeItemsToCreate = await Promise.all(
    directTemplateEnvelope.envelopeItems.map(async (item, i) => {
      const buffer = await getFileServerSide(item.documentData);

      const titleToUse = item.title || directTemplateEnvelope.title;

      const preparedDocumentData = await preparePdfFileServerSide({
        name: titleToUse,
        type: 'application/pdf',
        arrayBuffer: async () => Promise.resolve(buffer),
      });

      const newEnvelopeItemId = prefixedId('envelope_item');

      oldEnvelopeItemToNewEnvelopeItemIdMap[item.id] = newEnvelopeItemId;

      return {
        id: newEnvelopeItemId,
        title: titleToUse.endsWith('.pdf') ? titleToUse.slice(0, -4) : titleToUse,
        documentData: { type: preparedDocumentData.type, data: preparedDocumentData.data },
        order: item.order !== undefined ? item.order : i + 1,
      };
    }),
  );

  return {
    directTemplateEnvelope,
    signatureLevel,
    directTemplateEnvelopeLegacyId,
    directTemplateExternalId,
    initialRequestTime,
    settings,
    envelopeItemsToCreate,
    templateAuthOptions,
    nonDirectTemplateRecipients,
    directTemplateRecipientAuthOptions,
    directTemplateRecipient,
    directTemplateNonSignatureFields,
    directTemplateSignatureFields,
    oldEnvelopeItemToNewEnvelopeItemIdMap,
    directRecipientEmail,
    directRecipientName,
    derivedRecipientAccessAuth,
    nextSigner,
    user,
    requestMetadata,
    derivedDocumentMeta,
  };
};
