import { prisma } from '@documenso/prisma';
import type { DocumentMeta, Envelope, Recipient, User } from '@prisma/client';
import { DocumentStatus, EnvelopeType, RecipientRole, SendStatus, WebhookTriggerEvents } from '@prisma/client';

import { AppError, AppErrorCode } from '../../errors/app-error';
import { jobs } from '../../jobs/client';
import { DOCUMENT_AUDIT_LOG_TYPE } from '../../types/document-audit-logs';
import { extractDerivedDocumentEmailSettings } from '../../types/document-email';
import { mapEnvelopeToWebhookDocumentPayload, ZWebhookDocumentSchema } from '../../types/webhook-payload';
import type { ApiRequestMetadata } from '../../universal/extract-request-metadata';
import { isDocumentCompleted } from '../../utils/document';
import { createDocumentAuditLogData } from '../../utils/document-audit-logs';
import { type EnvelopeIdOptions, unsafeBuildEnvelopeIdQuery } from '../../utils/envelope';
import { isRecipientEmailValidForSending } from '../../utils/recipients';
import { getEmailContext } from '../email/get-email-context';
import { getEnvelopeWhereInput } from '../envelope/get-envelope-by-id';
import { triggerWebhook } from '../webhooks/trigger/trigger-webhook';

export type DeleteDocumentOptions = {
  id: EnvelopeIdOptions;
  userId: number;
  teamId: number;
  requestMetadata: ApiRequestMetadata;
};

export const deleteDocument = async ({ id, userId, teamId, requestMetadata }: DeleteDocumentOptions) => {
  const user = await prisma.user.findUnique({
    where: {
      id: userId,
    },
  });

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

  // Note: This is an unsafe request. It is used purely to resolve the recipient
  // self-hide path below. The authoritative delete authorization is performed
  // via the visibility-aware `getEnvelopeWhereInput` helper.
  const envelope = await prisma.envelope.findUnique({
    where: unsafeBuildEnvelopeIdQuery(id, EnvelopeType.DOCUMENT),
    include: {
      recipients: true,
      documentMeta: true,
    },
  });

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

  // Determine whether the user has authorized delete access using the
  // visibility-aware helper. This enforces ownership OR (team membership AND
  // the document's visibility is permitted for the member's role) OR team-email
  // access. A bare team member without sufficient visibility will resolve to
  // null here and therefore must not be able to delete the document.
  const hasDeleteAccess = await getEnvelopeWhereInput({
    id,
    userId,
    teamId,
    type: EnvelopeType.DOCUMENT,
  })
    .then(({ envelopeWhereInput }) =>
      prisma.envelope.findFirst({
        where: envelopeWhereInput,
        select: { id: true },
      }),
    )
    .then((result) => Boolean(result))
    .catch(() => false);

  const userRecipient = envelope.recipients.find((recipient) => recipient.email === user.email);

  if (!hasDeleteAccess && !userRecipient) {
    throw new AppError(AppErrorCode.UNAUTHORIZED, {
      message: 'Not allowed',
    });
  }

  // Handle hard or soft deleting the actual document if user has permission.
  if (hasDeleteAccess) {
    const updatedEnvelope = await handleDocumentOwnerDelete({
      envelope,
      user,
      requestMetadata,
    });

    const envelopeForWebhook = { ...envelope, ...(updatedEnvelope ?? {}) };

    await triggerWebhook({
      event: WebhookTriggerEvents.DOCUMENT_CANCELLED,
      data: ZWebhookDocumentSchema.parse(mapEnvelopeToWebhookDocumentPayload(envelopeForWebhook)),
      userId,
      teamId,
    });
  }

  // Continue to hide the document from the user if they are a recipient.
  // Dirty way of doing this but it's faster than refetching the document.
  if (userRecipient?.documentDeletedAt === null) {
    await prisma.recipient
      .update({
        where: {
          id: userRecipient.id,
        },
        data: {
          documentDeletedAt: new Date().toISOString(),
        },
      })
      .catch(() => {
        // Do nothing.
      });
  }

  return envelope;
};

type HandleDocumentOwnerDeleteOptions = {
  envelope: Envelope & {
    recipients: Recipient[];
    documentMeta: DocumentMeta | null;
  };
  user: User;
  requestMetadata: ApiRequestMetadata;
};

const handleDocumentOwnerDelete = async ({ envelope, user, requestMetadata }: HandleDocumentOwnerDeleteOptions) => {
  if (envelope.deletedAt) {
    return;
  }

  const { emailLanguage, emailsDisabled } = await getEmailContext({
    emailType: 'RECIPIENT',
    source: {
      type: 'team',
      teamId: envelope.teamId,
    },
    meta: envelope.documentMeta,
  });

  // Soft delete completed documents.
  if (isDocumentCompleted(envelope.status)) {
    return await prisma.$transaction(async (tx) => {
      await tx.documentAuditLog.create({
        data: createDocumentAuditLogData({
          envelopeId: envelope.id,
          type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
          metadata: requestMetadata,
          data: {
            type: 'SOFT',
          },
        }),
      });

      return await tx.envelope.update({
        where: {
          id: envelope.id,
        },
        data: {
          deletedAt: new Date().toISOString(),
        },
      });
    });
  }

  // Hard delete draft and pending documents.
  const deletedEnvelope = await prisma.$transaction(async (tx) => {
    // Currently redundant since deleting a document will delete the audit logs.
    // However may be useful if we disassociate audit logs and documents if required.
    await tx.documentAuditLog.create({
      data: createDocumentAuditLogData({
        envelopeId: envelope.id,
        type: DOCUMENT_AUDIT_LOG_TYPE.DOCUMENT_DELETED,
        metadata: requestMetadata,
        data: {
          type: 'HARD',
        },
      }),
    });

    return await tx.envelope.delete({
      where: {
        id: envelope.id,
        status: {
          not: DocumentStatus.COMPLETED,
        },
      },
    });
  });

  const isEnvelopeDeleteEmailEnabled = extractDerivedDocumentEmailSettings(envelope.documentMeta).documentDeleted;

  // Skip sending if the email is disabled for this document or the organisation
  // has email sending disabled entirely.
  if (!isEnvelopeDeleteEmailEnabled || emailsDisabled) {
    return deletedEnvelope;
  }

  // Enqueue cancellation emails as a background job. The envelope (and its
  // documentMeta) is hard-deleted above, so the job can't look it up later —
  // pass a self-contained payload with the recipients to notify.
  const recipientsToNotify = envelope.recipients
    .filter(
      (recipient) =>
        recipient.sendStatus === SendStatus.SENT &&
        recipient.role !== RecipientRole.CC &&
        isRecipientEmailValidForSending(recipient),
    )
    .map((recipient) => ({
      email: recipient.email,
      name: recipient.name,
    }));

  if (recipientsToNotify.length > 0) {
    await jobs.triggerJob({
      name: 'send.document.deleted.emails',
      payload: {
        teamId: envelope.teamId,
        documentName: envelope.title,
        inviterName: user.name || undefined,
        inviterEmail: user.email,
        meta: envelope.documentMeta
          ? {
              emailId: envelope.documentMeta.emailId,
              emailReplyTo: envelope.documentMeta.emailReplyTo,
              language: emailLanguage,
            }
          : null,
        recipients: recipientsToNotify,
      },
    });
  }

  return deletedEnvelope;
};
