import m, { Children, Component, Vnode } from "mithril"
import stream from "mithril/stream"
import Stream from "mithril/stream"
import { Editor, ImagePasteEvent } from "../../../common/gui/editor/Editor"
import type { Attachment, InitAsResponseArgs, SendMailModel } from "../../../common/mailFunctionality/SendMailModel.js"
import { Dialog } from "../../../common/gui/base/Dialog"
import { InfoLink, lang } from "../../../common/misc/LanguageViewModel"
import { MailboxDetail, MailboxModel } from "../../../common/mailFunctionality/MailboxModel.js"
import { checkApprovalStatus } from "../../../common/misc/LoginUtils"
import { locator } from "../../../common/api/main/CommonLocator"
import {
	ALLOWED_IMAGE_FORMATS,
	ConversationType,
	ExternalImageRule,
	FeatureType,
	Keys,
	MailAuthenticationStatus,
	MailMethod,
} from "../../../common/api/common/TutanotaConstants"
import { TooManyRequestsError } from "../../../common/api/common/error/RestError"
import type { DialogHeaderBarAttrs } from "../../../common/gui/base/DialogHeaderBar"
import { ButtonType } from "../../../common/gui/base/Button.js"
import { attachDropdown, createDropdown, DropdownChildAttrs } from "../../../common/gui/base/Dropdown.js"
import { isApp, isBrowser, isDesktop } from "../../../common/api/common/Env"
import { Icons } from "../../../common/gui/base/icons/Icons"
import { AnimationPromise, animations, height, opacity } from "../../../common/gui/animation/Animations"
import type { TextFieldAttrs } from "../../../common/gui/base/TextField.js"
import { Autocomplete, TextField } from "../../../common/gui/base/TextField.js"
import { chooseAndAttachFile, cleanupInlineAttachments, createAttachmentBubbleAttrs, getConfidentialStateMessage } from "./MailEditorViewModel"
import { ExpanderPanel } from "../../../common/gui/base/Expander"
import { windowFacade } from "../../../common/misc/WindowFacade"
import { UserError } from "../../../common/api/main/UserError"
import { showProgressDialog } from "../../../common/gui/dialogs/ProgressDialog"
import { getHtmlSanitizer, HtmlSanitizer } from "../../../common/misc/HtmlSanitizer"
import { DropDownSelector } from "../../../common/gui/base/DropDownSelector.js"
import {
	Contact,
	ContactTypeRef,
	ConversationEntry,
	createTranslationGetIn,
	File as TutanotaFile,
	Mail,
	MailboxProperties,
	MailDetails,
} from "../../../common/api/entities/tutanota/TypeRefs.js"
import { FileOpenError } from "../../../common/api/common/error/FileOpenError"
import {
	assertNotNull,
	cleanMatch,
	debounce,
	downcast,
	isNotNull,
	lazy,
	minutesToMillis,
	noOp,
	ofClass,
	secondsToMillis,
	throttle,
	typedValues,
} from "@tutao/tutanota-utils"
import { createInlineImage, replaceCidsWithInlineImages, replaceInlineImagesWithCids } from "../view/MailGuiUtils"
import { client } from "../../../common/misc/ClientDetector"
import { appendEmailSignature } from "../signature/Signature"
import { showTemplatePopupInEditor } from "../../templates/view/TemplatePopup"
import { registerTemplateShortcutListener } from "../../templates/view/TemplateShortcutListener"
import { TemplatePopupModel } from "../../templates/model/TemplatePopupModel"
import { createKnowledgeBaseDialogInjection } from "../../knowledgebase/view/KnowledgeBaseDialog"
import { KnowledgeBaseModel } from "../../knowledgebase/model/KnowledgeBaseModel"
import { styles } from "../../../common/gui/styles"
import { showMinimizedMailEditor } from "../view/MinimizedMailEditorOverlay"
import { MinimizedMailEditorViewModel, SaveErrorReason, SaveStatus, SaveStatusEnum } from "../model/MinimizedMailEditorViewModel"
import { fileListToArray, FileReference, isTutanotaFile } from "../../../common/api/common/utils/FileUtils"
import { parseMailtoUrl } from "../../../common/misc/parsing/MailAddressParser"
import { CancelledError } from "../../../common/api/common/error/CancelledError"
import { Shortcut } from "../../../common/misc/KeyManager"
import { Recipients, RecipientType } from "../../../common/api/common/recipients/Recipient"
import { showUserError } from "../../../common/misc/ErrorHandlerImpl"
import { MailRecipientsTextField } from "../../../common/gui/MailRecipientsTextField.js"
import { getContactDisplayName } from "../../../common/contactsFunctionality/ContactUtils.js"
import { ResolvableRecipient } from "../../../common/api/main/RecipientsModel"

import { animateToolbar, RichTextToolbar } from "../../../common/gui/base/RichTextToolbar.js"
import { readLocalFiles } from "../../../common/file/FileController"
import { IconButton, IconButtonAttrs } from "../../../common/gui/base/IconButton.js"
import { ToggleButton, ToggleButtonAttrs } from "../../../common/gui/base/buttons/ToggleButton.js"
import { BootIcons } from "../../../common/gui/base/icons/BootIcons.js"
import { ButtonSize } from "../../../common/gui/base/ButtonSize.js"
import { DialogInjectionRightAttrs } from "../../../common/gui/base/DialogInjectionRight.js"
import { KnowledgebaseDialogContentAttrs } from "../../knowledgebase/view/KnowledgeBaseDialogContent.js"
import { RecipientsSearchModel } from "../../../common/misc/RecipientsSearchModel.js"
import { createDataFile, DataFile } from "../../../common/api/common/DataFile.js"
import { AttachmentBubble } from "../../../common/gui/AttachmentBubble.js"
import { ContentBlockingStatus } from "../view/MailViewerViewModel.js"
import { canSeeTutaLinks } from "../../../common/gui/base/GuiUtils.js"
import { BannerButtonAttrs, InfoBanner } from "../../../common/gui/base/InfoBanner.js"
import { isCustomizationEnabledForCustomer } from "../../../common/api/common/utils/CustomerUtils.js"
import { isOfflineError } from "../../../common/api/common/utils/ErrorUtils.js"
import { TranslationService } from "../../../common/api/entities/tutanota/Services.js"
import { PasswordField } from "../../../common/misc/passwords/PasswordField.js"
import { InlineImages } from "../../../common/mailFunctionality/inlineImagesUtils.js"
import {
	checkAttachmentSize,
	createNewContact,
	dialogTitleTranslationKey,
	getEnabledMailAddressesWithUser,
	getMailAddressDisplayText,
	RecipientField,
} from "../../../common/mailFunctionality/SharedMailUtils.js"
import { mailLocator } from "../../mailLocator.js"

import { isDarkTheme, theme } from "../../../common/gui/theme"
import { px, size } from "../../../common/gui/size"

import type { AutosaveFacade, LocalAutosavedDraftData } from "../../../common/api/worker/facades/lazy/AutosaveFacade"
import { showOverwriteDraftDialog, showOverwriteRemoteDraftDialog } from "./OverwriteDraftDialogs"

// Interval where we save drafts locally.
//
// This will save while the user is typing, thus the user only loses a few seconds of progress at most if the app
// unexpectedly closes (crash, power outage, etc.).
const AUTOSAVE_LOCAL_TIMEOUT: number = secondsToMillis(5)

// If the editor is left untouched for this amount of time, then the draft will automatically save to the server.
const AUTOSAVE_REMOTE_TIMEOUT: number = minutesToMillis(5)

export type MailEditorAttrs = {
	model: SendMailModel
	doBlockExternalContent: Stream<boolean>
	doShowToolbar: Stream<boolean>
	onChange?: () => unknown
	onclose?: (...args: Array<any>) => any
	selectedNotificationLanguage: Stream<string>
	dialog: lazy<Dialog>
	templateModel: TemplatePopupModel | null
	knowledgeBaseInjection: (editor: Editor) => Promise<DialogInjectionRightAttrs<KnowledgebaseDialogContentAttrs> | null>
	search: RecipientsSearchModel
	alwaysBlockExternalContent: boolean
}

export function createMailEditorAttrs(
	model: SendMailModel,
	doBlockExternalContent: boolean,
	doFocusEditorOnLoad: boolean,
	dialog: lazy<Dialog>,
	templateModel: TemplatePopupModel | null,
	knowledgeBaseInjection: (editor: Editor) => Promise<DialogInjectionRightAttrs<KnowledgebaseDialogContentAttrs> | null>,
	search: RecipientsSearchModel,
	alwaysBlockExternalContent: boolean,
): MailEditorAttrs {
	return {
		model,
		doBlockExternalContent: stream(doBlockExternalContent),
		doShowToolbar: stream<boolean>(false),
		selectedNotificationLanguage: stream(""),
		dialog,
		templateModel,
		knowledgeBaseInjection: knowledgeBaseInjection,
		search,
		alwaysBlockExternalContent,
	}
}

export class MailEditor implements Component<MailEditorAttrs> {
	private attrs: MailEditorAttrs

	editor: Editor

	private readonly recipientFieldTexts = {
		to: stream(""),
		cc: stream(""),
		bcc: stream(""),
	}

	mentionedInlineImages: Array<string>
	inlineImageElements: Array<HTMLElement>
	templateModel: TemplatePopupModel | null
	knowledgeBaseInjection: DialogInjectionRightAttrs<KnowledgebaseDialogContentAttrs> | null = null
	sendMailModel: SendMailModel
	private areDetailsExpanded: boolean
	private recipientShowConfidential: Map<string, boolean> = new Map()
	private blockExternalContent: boolean
	private readonly alwaysBlockExternalContent: boolean = false
	// if we're set to block external content, but there is no content to block,
	// we don't want to show the banner.
	private blockedExternalContent: number = 0
	private shouldCollapseQuotedReply: boolean = true
	private collapsedReply: HTMLElement | null = null
	private forceLightMode: boolean = false

	private readonly htmlSanitizer: HtmlSanitizer = getHtmlSanitizer()

	constructor(vnode: Vnode<MailEditorAttrs>) {
		const a = vnode.attrs
		this.attrs = a
		this.inlineImageElements = []
		this.mentionedInlineImages = []
		const model = a.model
		this.sendMailModel = model
		this.templateModel = a.templateModel
		this.blockExternalContent = a.doBlockExternalContent()
		this.alwaysBlockExternalContent = a.alwaysBlockExternalContent

		// if we have any CC/BCC recipients, we should show these so, should the user send the mail, they know where it will be going to
		this.areDetailsExpanded = model.bccRecipients().length + model.ccRecipients().length > 0

		this.editor = new Editor(
			200,
			(html, isPaste) => {
				const sanitized = this.htmlSanitizer.sanitizeFragment(html, {
					blockExternalContent: !isPaste && this.blockExternalContent,
				})
				this.blockedExternalContent = sanitized.blockedExternalContent

				this.mentionedInlineImages = sanitized.inlineImageCids
				return sanitized.fragment
			},
			null,
		)

		const onEditorChanged = () => {
			cleanupInlineAttachments(this.editor.getDOM(), this.inlineImageElements, model.getAttachments())
			model.markAsChangedIfNecessary(true)
			m.redraw()
		}

		// call this async because the editor is not initialized before this mail editor dialog is shown
		this.editor.initialized.promise.then(() => {
			this.editor.setHTML(model.getBody())

			const editorDom = this.editor.getDOM()

			this.processInlineImages()
			const htmlBeforeProcessQuotedReply = editorDom.innerHTML
			this.editor.squire.modifyDocument(() => {
				this.processQuotedReply(editorDom)
			})

			// Add mutation observer to remove attachments when corresponding DOM element is removed
			new MutationObserver(onEditorChanged).observe(this.editor.getDOM(), {
				attributes: false,
				childList: true,
				subtree: true,
			})
			// since the editor is the source for the body text, the model won't know if the body has changed unless we tell it
			this.editor.addChangeListener(() => {
				const editorDom = this.editor.getDOM()

				// Reset collapsed reply processing when all changes are undone
				if (editorDom.innerHTML === htmlBeforeProcessQuotedReply) {
					this.shouldCollapseQuotedReply = true
					this.collapsedReply = null
				}
				this.editor.squire.modifyDocument(() => {
					// We process with modifyDocument to not raise an input event as that would cause an infinite loop
					this.processQuotedReply(editorDom)
				})

				model.setBody(this.getBodyHtml(editorDom))
			})
			this.editor.addEventListener("pasteImage", ({ detail }: ImagePasteEvent) => {
				const items = Array.from(detail.clipboardData.items)
				const imageItems = items.filter((item) => /image/.test(item.type))
				if (!imageItems.length) {
					return false
				}
				const file = imageItems[0]?.getAsFile()
				if (file == null) {
					return false
				}
				const reader = new FileReader()
				reader.onload = () => {
					if (reader.result == null || "string" === typeof reader.result) {
						return
					}
					const newInlineImages = [createDataFile(file.name, file.type, new Uint8Array(reader.result))]
					model.attachFiles(newInlineImages)
					this.insertInlineImages(model, newInlineImages)
				}
				reader.readAsArrayBuffer(file)
			})

			if (a.templateModel) {
				a.templateModel.init().then((templateModel) => {
					// add this event listener to handle quick selection of templates inside the editor
					registerTemplateShortcutListener(this.editor, templateModel)
				})
			}
		})

		model.onMailChanged.map(() => {
			this.attrs.onChange?.()
			m.redraw()
		})
		// Leftover text in recipient field is an error
		model.setOnBeforeSendFunction(() => {
			let invalidText = ""
			for (const leftoverText of typedValues(this.recipientFieldTexts)) {
				if (leftoverText().trim() !== "") {
					invalidText += "\n" + leftoverText().trim()
				}
			}

			if (invalidText !== "") {
				throw new UserError(lang.makeTranslation("invalidRecipients_msg", lang.get("invalidRecipients_msg") + invalidText))
			}
		})
		const dialog = a.dialog()

		if (model.getConversationType() === ConversationType.REPLY || model.toRecipients().length) {
			dialog.setFocusOnLoadFunction(() => {
				this.editor.initialized.promise.then(() => this.editor.focus())
			})
		}

		const shortcuts: Shortcut[] = [
			{
				key: Keys.SPACE,
				ctrlOrCmd: true,
				exec: () => this.openTemplates(),
				help: "openTemplatePopup_msg",
			}, // B (bold), I (italic), and U (underline) are handled by squire
			{
				key: Keys.B,
				ctrlOrCmd: true,
				exec: noOp,
				help: "formatTextBold_msg",
			},
			{
				key: Keys.I,
				ctrlOrCmd: true,
				exec: noOp,
				help: "formatTextItalic_msg",
			},
			{
				key: Keys.U,
				ctrlOrCmd: true,
				exec: noOp,
				help: "formatTextUnderline_msg",
			},
		]
		for (const shortcut of shortcuts) {
			dialog.addShortcut(shortcut)
		}
		this.editor.initialized.promise.then(() => {
			a.knowledgeBaseInjection(this.editor).then((injection) => {
				this.knowledgeBaseInjection = injection
				m.redraw()
			})
		})
	}

	private processQuotedReply(dom: HTMLElement): void {
		if (!this.shouldCollapseQuotedReply && this.collapsedReply == null) {
			// Nothing to collapse
			return
		}

		const alreadyCollapsedReply: HTMLElement | null = dom.querySelector("[tuta-collapsed-quote=true]")
		if (alreadyCollapsedReply == null && this.collapsedReply != null) {
			// Collapsed reply was removed from the dom at some point

			// It's possible there are more than one collapsable reply, but we only ever collapse one, so when it's removed, we expand,
			// this is to avoid collapsing another reply, which can lead to it being incorrectly replaced by the old one when expanding after undo.
			this.shouldCollapseQuotedReply = false
			return
		}

		// There's a chance there are more than one collapsable reply (this is the case for draft with inline replies),
		// we only select the last one, since it's likely to be the one with the most nested replies.
		let collapsableReply: HTMLElement | null = null
		if (this.collapsedReply == null) {
			this.collapsedReply = collapsableReply = dom.querySelector(".text_editor>.tutanota_quote:last-of-type>.tutanota_quote:last-of-type")
		}

		if (this.shouldCollapseQuotedReply) {
			const elementToReplace = alreadyCollapsedReply ?? collapsableReply
			if (elementToReplace != null) {
				// We recreate the already collapsed reply to re-add the click handler that would otherwise be removed by undo
				this.collapseQuotedReply(elementToReplace)
			} else {
				// Nothing to collapse
				this.shouldCollapseQuotedReply = false
			}
		} else {
			// Expands removed collapsed reply after undo
			this.expandQuotedReply(assertNotNull(alreadyCollapsedReply))
		}
	}

	private collapseQuotedReply(elementToReplace: HTMLElement): void {
		const quoteWrap = document.createElement("div")
		quoteWrap.setAttribute("tuta-collapsed-quote", "true")

		elementToReplace.replaceWith(quoteWrap)

		const quoteIndicator = document.createElement("div")
		quoteIndicator.style.borderLeft = `2px solid ${theme.outline}`
		quoteIndicator.style.paddingLeft = "2px"
		quoteIndicator.style.marginTop = px(size.vpad)

		m.render(
			quoteIndicator,
			m(
				".ml-button.fit-content",
				{
					style: {
						borderRadius: "25%",
						border: `1px solid ${theme.outline}`,
					},
				},
				m(IconButton, {
					icon: Icons.More,
					title: "showText_action",
					size: ButtonSize.Normal,
					click: () => this.expandQuotedReply(quoteWrap),
				}),
			),
		)

		quoteWrap.appendChild(quoteIndicator)
	}

	private expandQuotedReply(quoteWrap: HTMLElement): void {
		this.shouldCollapseQuotedReply = false
		this.editor.squire.modifyDocument(() => {
			quoteWrap.replaceWith(assertNotNull(this.collapsedReply))
		})
	}

	private getBodyHtml(editorDom: HTMLElement): string {
		const modifiedDom = replaceInlineImagesWithCids(editorDom)
		const collapsedQuote = modifiedDom.querySelector("[tuta-collapsed-quote=true]")
		if (collapsedQuote != null) {
			// Note that the user may have deleted the quote, but if not, we can expand it here
			collapsedQuote.replaceWith(assertNotNull(this.collapsedReply))
		}
		return modifiedDom.innerHTML
	}

	private downloadInlineImage(model: SendMailModel, cid: string) {
		const tutanotaFiles = model.getAttachments().filter((attachment) => isTutanotaFile(attachment))
		const inlineAttachment = tutanotaFiles.find((attachment) => attachment.cid === cid)

		if (inlineAttachment && isTutanotaFile(inlineAttachment)) {
			locator.fileController.open(inlineAttachment).catch(ofClass(FileOpenError, () => Dialog.message("canNotOpenFileOnDevice_msg")))
		}
	}

	view(vnode: Vnode<MailEditorAttrs>): Children {
		const a = vnode.attrs
		this.attrs = a
		const { model } = a
		this.sendMailModel = model

		const showConfidentialButton = model.containsExternalRecipients()
		const isConfidential = model.isConfidential() && showConfidentialButton
		const confidentialButtonAttrs: ToggleButtonAttrs = {
			title: "confidential_action",
			onToggled: (_, e) => {
				e.stopPropagation()
				model.setConfidential(!model.isConfidential())
			},
			icon: model.isConfidential() ? Icons.Lock : Icons.Unlock,
			toggled: model.isConfidential(),
			size: ButtonSize.Compact,
		}
		const attachFilesButtonAttrs: IconButtonAttrs = {
			title: "attachFiles_action",
			click: (ev, dom) => chooseAndAttachFile(model, dom.getBoundingClientRect()).then(() => m.redraw()),
			icon: Icons.Attachment,
			size: ButtonSize.Compact,
		}

		const darkTheme = isDarkTheme()

		// The actual client theme can change at any time, so we do not want to actually do anything in the case that
		// the client suddenly switches to the light theme.
		const forcedLightMode = darkTheme && this.forceLightMode

		const plaintextFormatting = locator.logins.getUserController().props.sendPlaintextOnly
		this.editor.setCreatesLists(!plaintextFormatting)

		const toolbarButton = () =>
			!plaintextFormatting
				? m(ToggleButton, {
						title: "showRichTextToolbar_action",
						icon: Icons.FontSize,
						size: ButtonSize.Compact,
						toggled: a.doShowToolbar(),
						onToggled: (_, e) => {
							a.doShowToolbar(!a.doShowToolbar())
							// Stop the subject bar from being focused
							e.stopPropagation()
							this.editor.focus()
						},
					})
				: null

		const subjectFieldAttrs: TextFieldAttrs = {
			label: "subject_label",
			helpLabel: () => getConfidentialStateMessage(model.isConfidential()),
			value: model.getSubject(),
			oninput: (val) => model.setSubject(val),
			injectionsRight: () =>
				m(".flex.end.ml-between-s.items-center", [
					isDarkTheme()
						? m(IconButton, {
								title: "viewInLightMode_action",
								click: (e) => {
									this.forceLightMode = !forcedLightMode
									// Stop the subject bar from being focused
									e.stopPropagation()
									this.editor.focus()
									m.redraw()
								},
								// reflect the current mode in the bulb
								icon: forcedLightMode ? Icons.Bulb : Icons.BulbOutline,
								size: ButtonSize.Compact,
							})
						: null,
					toolbarButton(),
					showConfidentialButton ? m(ToggleButton, confidentialButtonAttrs) : null,
					this.knowledgeBaseInjection ? this.renderToggleKnowledgeBase(this.knowledgeBaseInjection) : null,
					m(IconButton, attachFilesButtonAttrs),
				]),
		}

		const attachmentBubbleAttrs = createAttachmentBubbleAttrs(model, this.inlineImageElements)

		let editCustomNotificationMailAttrs: IconButtonAttrs | null = null

		if (locator.logins.getUserController().isGlobalAdmin()) {
			editCustomNotificationMailAttrs = attachDropdown({
				mainButtonAttrs: {
					title: "more_label",
					icon: Icons.More,
					size: ButtonSize.Compact,
				},
				childAttrs: () => [
					{
						label: "add_action",
						click: () => {
							import("../../../common/settings/EditNotificationEmailDialog.js").then(({ showAddOrEditNotificationEmailDialog }) =>
								showAddOrEditNotificationEmailDialog(locator.logins.getUserController()),
							)
						},
					},
					{
						label: "edit_action",
						click: () => {
							import("../../../common/settings/EditNotificationEmailDialog.js").then(({ showAddOrEditNotificationEmailDialog }) =>
								showAddOrEditNotificationEmailDialog(locator.logins.getUserController(), model.getSelectedNotificationLanguageCode()),
							)
						},
					},
				],
			})
		}

		return m(
			"#mail-editor.full-height.text.touch-callout.flex.flex-column",
			{
				onclick: (e: MouseEvent) => {
					if (e.target === this.editor.getDOM()) {
						this.editor.focus()
					}
				},
				ondragover: (ev: DragEvent) => {
					// do not check the data transfer here because it is not always filled, e.g. in Safari
					ev.stopPropagation()
					ev.preventDefault()
				},
				ondrop: (ev: DragEvent) => {
					if (ev.dataTransfer?.files && ev.dataTransfer.files.length > 0) {
						let nativeFiles = fileListToArray(ev.dataTransfer.files)
						readLocalFiles(nativeFiles)
							.then((dataFiles) => {
								model.attachFiles(dataFiles as any)
								m.redraw()
							})
							.catch((e) => {
								console.log(e)
								return Dialog.message("couldNotAttachFile_msg")
							})
						ev.stopPropagation()
						ev.preventDefault()
					}
				},
			},
			[
				m(".rel", this.renderRecipientField(RecipientField.TO, this.recipientFieldTexts.to, a.search)),
				m(
					".rel",
					m(
						ExpanderPanel,
						{
							expanded: this.areDetailsExpanded,
						},
						m(".details", [
							this.renderRecipientField(RecipientField.CC, this.recipientFieldTexts.cc, a.search),
							this.renderRecipientField(RecipientField.BCC, this.recipientFieldTexts.bcc, a.search),
						]),
					),
				),
				m(".wrapping-row", [
					m(
						"",
						{
							style: {
								"min-width": "250px",
							},
						},
						m(DropDownSelector, {
							label: "sender_label",
							items: getEnabledMailAddressesWithUser(model.mailboxDetails, model.user().userGroupInfo)
								.sort()
								.map((mailAddress) => ({
									name: mailAddress,
									value: mailAddress,
								})),
							selectedValue: a.model.getSender(),
							selectedValueDisplay: getMailAddressDisplayText(a.model.getSenderName(), a.model.getSender(), false),
							selectionChangedHandler: (selection: string) => model.setSender(selection),
							dropdownWidth: 250,
						}),
					),
					isConfidential
						? m(
								".flex",
								{
									style: {
										"min-width": "250px",
									},
									oncreate: (vnode) => {
										const htmlDom = vnode.dom as HTMLElement
										htmlDom.style.opacity = "0"
										return animations.add(htmlDom, opacity(0, 1, true))
									},
									onbeforeremove: (vnode) => {
										const htmlDom = vnode.dom as HTMLElement
										htmlDom.style.opacity = "1"
										return animations.add(htmlDom, opacity(1, 0, true))
									},
								},
								[
									m(
										".flex-grow",
										m(DropDownSelector, {
											label: "notificationMailLanguage_label",
											items: model.getAvailableNotificationTemplateLanguages().map((language) => {
												return {
													name: lang.get(language.textId),
													value: language.code,
												}
											}),
											selectedValue: model.getSelectedNotificationLanguageCode(),
											selectionChangedHandler: (v: string) => model.setSelectedNotificationLanguageCode(v),
											dropdownWidth: 250,
										}),
									),
									editCustomNotificationMailAttrs
										? m(".pt.flex-no-grow.flex-end.border-bottom.flex.items-center", m(IconButton, editCustomNotificationMailAttrs))
										: null,
								],
							)
						: null,
				]),
				isConfidential ? this.renderPasswordFields() : null,
				m(".row", m(TextField, subjectFieldAttrs)),
				m(
					".flex-start.flex-wrap.mt-s.mb-s.gap-hpad",
					attachmentBubbleAttrs.map((a) => m(AttachmentBubble, a)),
				),
				model.getAttachments().length > 0 ? m("hr.hr") : null,
				this.renderExternalContentBanner(this.attrs),
				a.doShowToolbar() ? this.renderToolbar(model) : null,
				m(
					".pt-s.text.scroll-x.break-word-links.flex.flex-column.flex-grow" + (forcedLightMode ? ".bg-white.content-black.bg-fix-quoted" : ""),
					{
						onclick: () => this.editor.focus(),
					},
					m(this.editor),
				),
				m(".pb"),
			],
		)
	}

	private renderExternalContentBanner(attrs: MailEditorAttrs): Children | null {
		if (!this.blockExternalContent || this.alwaysBlockExternalContent || this.blockedExternalContent === 0) {
			return null
		}

		const showButton: BannerButtonAttrs = {
			label: "showBlockedContent_action",
			click: () => {
				this.updateExternalContentStatus(ContentBlockingStatus.Show)
				this.processInlineImages()
			},
		}

		return m(InfoBanner, {
			message: "contentBlocked_msg",
			icon: Icons.Picture,
			helpLink: canSeeTutaLinks(attrs.model.logins) ? InfoLink.LoadImages : null,
			buttons: [showButton],
		})
	}

	private updateExternalContentStatus(status: ContentBlockingStatus) {
		this.blockExternalContent = status === ContentBlockingStatus.Block || status === ContentBlockingStatus.AlwaysBlock

		const sanitized = this.htmlSanitizer.sanitizeHTML(this.editor.getHTML(), {
			blockExternalContent: this.blockExternalContent,
		})

		this.editor.setHTML(sanitized.html)
	}

	private processInlineImages() {
		this.inlineImageElements = replaceCidsWithInlineImages(this.editor.getDOM(), this.sendMailModel.loadedInlineImages, (cid, event, dom) => {
			const downloadClickHandler = createDropdown({
				lazyButtons: () => [
					{
						label: "download_action",
						click: () => this.downloadInlineImage(this.sendMailModel, cid),
					},
				],
			})
			downloadClickHandler(downcast(event), dom)
		})
	}

	private renderToggleKnowledgeBase(knowledgeBaseInjection: DialogInjectionRightAttrs<KnowledgebaseDialogContentAttrs>) {
		return m(ToggleButton, {
			title: "openKnowledgebase_action",
			toggled: knowledgeBaseInjection.visible(),
			onToggled: () => {
				if (knowledgeBaseInjection.visible()) {
					knowledgeBaseInjection.visible(false)
				} else {
					knowledgeBaseInjection.componentAttrs.model.sortEntriesByMatchingKeywords(this.editor.getValue())
					knowledgeBaseInjection.visible(true)
					knowledgeBaseInjection.componentAttrs.model.init()
				}
			},
			icon: Icons.Book,
			size: ButtonSize.Compact,
		})
	}

	private renderToolbar(model: SendMailModel): Children {
		// Toolbar is not removed from DOM directly, only it's parent (array) is so we have to animate it manually.
		// m.fragment() gives us a vnode without actual DOM element so that we can run callback on removal
		return m.fragment(
			{
				onbeforeremove: ({ dom }) => animateToolbar(dom.children[0] as HTMLElement, false),
			},
			[
				m(RichTextToolbar, {
					editor: this.editor,
					//Inline images require transporting over IPC boundary and we have not implemented a suitable way yet
					imageButtonClickHandler: isApp()
						? null
						: (event: Event) => this.imageButtonClickHandler(model, (event.target as HTMLElement).getBoundingClientRect()),
					customButtonAttrs: this.templateModel
						? [
								{
									title: "openTemplatePopup_msg",
									click: () => {
										this.openTemplates()
									},
									icon: Icons.ListAlt,
									size: ButtonSize.Compact,
								},
							]
						: [],
				}),
				m("hr.hr"),
			],
		)
	}

	private async imageButtonClickHandler(model: SendMailModel, rect: DOMRect): Promise<void> {
		const files = await chooseAndAttachFile(model, rect, ALLOWED_IMAGE_FORMATS)
		if (!files || files.length === 0) return
		return await this.insertInlineImages(model, files)
	}

	private async insertInlineImages(model: SendMailModel, files: ReadonlyArray<DataFile | FileReference>): Promise<void> {
		for (const file of files) {
			const img = createInlineImage(file as DataFile)
			model.loadedInlineImages.set(img.cid, img)
			this.inlineImageElements.push(
				this.editor.insertImage(img.objectUrl, {
					cid: img.cid,
					style: "max-width: 100%",
				}),
			)
		}
		m.redraw()
	}

	private renderPasswordFields(): Children {
		return m(
			".external-recipients.overflow-hidden",
			{
				oncreate: (vnode) => this.animateHeight(vnode.dom as HTMLElement, true),
				onbeforeremove: (vnode) => this.animateHeight(vnode.dom as HTMLElement, false),
			},
			this.sendMailModel
				.allRecipients()
				.filter((r) => r.type === RecipientType.EXTERNAL)
				.map((recipient) => {
					if (!this.recipientShowConfidential.has(recipient.address)) this.recipientShowConfidential.set(recipient.address, false)

					return m(PasswordField, {
						oncreate: (vnode) => this.animateHeight(vnode.dom as HTMLElement, true),
						onbeforeremove: (vnode) => this.animateHeight(vnode.dom as HTMLElement, false),
						label: lang.getTranslation("passwordFor_label", { "{1}": recipient.address }),
						value: this.sendMailModel.getPassword(recipient.address),
						passwordStrength: this.sendMailModel.getPasswordStrength(recipient),
						status: "auto",
						autocompleteAs: Autocomplete.off,
						oninput: (val) => this.sendMailModel.setPassword(recipient.address, val),
					})
				}),
		)
	}

	private renderRecipientField(field: RecipientField, fieldText: Stream<string>, search: RecipientsSearchModel): Children {
		const label = (
			{
				to: "to_label",
				cc: "cc_label",
				bcc: "bcc_label",
			} as const
		)[field]

		return m(MailRecipientsTextField, {
			label,
			text: fieldText(),
			onTextChanged: (text) => fieldText(text),
			recipients: this.sendMailModel.getRecipientList(field),
			onRecipientAdded: async (address, name) => {
				try {
					await this.sendMailModel.addRecipient(field, { address, name })
				} catch (e) {
					if (isOfflineError(e)) {
						// we are offline but we want to show the error dialog only when we click on send.
					} else if (e instanceof TooManyRequestsError) {
						await Dialog.message("tooManyAttempts_msg")
					} else {
						throw e
					}
				}
			},
			onRecipientRemoved: (address) => this.sendMailModel.removeRecipientByAddress(address, field),
			getRecipientClickedDropdownAttrs: (address) => {
				const recipient = this.sendMailModel.getRecipient(field, address)!
				return this.getRecipientClickedContextButtons(recipient, field)
			},
			disabled: !this.sendMailModel.logins.isInternalUserLoggedIn(),
			injectionsRight:
				field === RecipientField.TO && this.sendMailModel.logins.isInternalUserLoggedIn()
					? m(
							"",
							m(ToggleButton, {
								title: "show_action",
								icon: BootIcons.Expand,
								size: ButtonSize.Compact,
								toggled: this.areDetailsExpanded,
								onToggled: (_, e) => {
									e.stopPropagation()
									this.areDetailsExpanded = !this.areDetailsExpanded
								},
							}),
						)
					: null,
			search,
		})
	}

	private async getRecipientClickedContextButtons(recipient: ResolvableRecipient, field: RecipientField): Promise<DropdownChildAttrs[]> {
		const { entity, contactModel } = this.sendMailModel

		const canEditBubbleRecipient = locator.logins.getUserController().isInternalUser() && !locator.logins.isEnabled(FeatureType.DisableContacts)

		const canRemoveBubble = locator.logins.getUserController().isInternalUser()

		const createdContactReceiver = (contactElementId: Id) => {
			const mailAddress = recipient.address

			contactModel.getContactListId().then((contactListId: string) => {
				if (!contactListId) return
				const id: IdTuple = [contactListId, contactElementId]
				entity.load(ContactTypeRef, id).then((contact: Contact) => {
					if (contact.mailAddresses.some((ma) => cleanMatch(ma.address, mailAddress))) {
						recipient.setName(getContactDisplayName(contact))
						recipient.setContact(contact)
					} else {
						this.sendMailModel.removeRecipient(recipient, field, false)
					}
				})
			})
		}

		const contextButtons: Array<DropdownChildAttrs> = []

		if (canEditBubbleRecipient) {
			if (recipient.contact && recipient.contact._id) {
				contextButtons.push({
					label: "editContact_label",
					click: () => {
						import("../../contacts/ContactEditor").then(({ ContactEditor }) => new ContactEditor(entity, recipient.contact).show())
					},
				})
			} else {
				contextButtons.push({
					label: "createContact_action",
					click: () => {
						// contact list
						contactModel.getContactListId().then((contactListId: Id) => {
							const newContact = createNewContact(locator.logins.getUserController().user, recipient.address, recipient.name)
							import("../../contacts/ContactEditor").then(({ ContactEditor }) => {
								// external users don't see edit buttons
								new ContactEditor(entity, newContact, assertNotNull(contactListId), createdContactReceiver).show()
							})
						})
					},
				})
			}
		}

		if (canRemoveBubble) {
			contextButtons.push({
				label: "remove_action",
				click: () => this.sendMailModel.removeRecipient(recipient, field, false),
			})
		}

		return contextButtons
	}

	private openTemplates() {
		if (this.templateModel) {
			this.templateModel.init().then((templateModel) => {
				showTemplatePopupInEditor(templateModel, this.editor, null, this.editor.getSelectedText())
			})
		}
	}

	private animateHeight(domElement: HTMLElement, fadein: boolean): AnimationPromise {
		let childHeight = domElement.offsetHeight
		return animations.add(domElement, fadein ? height(0, childHeight) : height(childHeight, 0)).then(() => {
			domElement.style.height = ""
		})
	}
}

/**
 * Creates a new Dialog with a MailEditor inside.
 * @param model
 * @param blockExternalContent
 * @param alwaysBlockExternalContent
 * @returns {Dialog}
 * @private
 */
async function createMailEditorDialog(model: SendMailModel, blockExternalContent = false, alwaysBlockExternalContent = false): Promise<Dialog> {
	let dialog: Dialog
	let mailEditorAttrs: MailEditorAttrs
	let isSending = false

	const save = async (manuallySave: boolean = true): Promise<SaveStatus> => {
		// Create an autosave now in case this errors
		await model.makeLocalAutosave()

		// Wait for sync if needed
		await model.waitForSaveReady()

		if (model.hasDraftDataChangedOnServer()) {
			const result: "cancel" | "overwrite" | "discard" = await showOverwriteRemoteDraftDialog(model.getMailRemotelyUpdatedAt())

			if (result === "cancel") {
				// the user closed the dialog without making a choice
				return { status: SaveStatusEnum.NotSaved, reason: SaveErrorReason.CancelledByUser }
			} else if (result === "discard") {
				// Discard and do not save
				await model.clearLocalAutosave()

				// if we have a minimized editor, delete it, too
				const draftMail = model.getDraft()
				const minimizedEditor = draftMail && mailLocator.minimizedMailModel.getEditorForDraft(draftMail)
				if (minimizedEditor != null) {
					mailLocator.minimizedMailModel.removeMinimizedEditor(minimizedEditor)
				}

				return {
					status: SaveStatusEnum.NotSaved,
					reason: SaveErrorReason.CancelledByUser,
				}
			}
			// Nothing needs to be done if the result is "overwrite", it will go on to the save code below
		}

		const savePromise = model.saveDraft(true, MailMethod.NONE)

		if (manuallySave) {
			await showProgressDialog("save_msg", savePromise)
		} else {
			await savePromise
		}

		await model.clearLocalAutosave()
		return { status: SaveStatusEnum.Saved }
	}

	// This will be called once the user stops typing.
	const autosaveRemote = debounce(AUTOSAVE_REMOTE_TIMEOUT, () => {
		// Autosaving should stop working if the dialog is closed.
		if (!dialog.visible) {
			return
		}

		// If the mail was already saved before this was triggered, don't save again.
		if (!model.hasMailChanged()) {
			return
		}

		// Don't try to save remotely until everything is synced (and can thus determine if there's a conflict).
		//
		// This is highly unlikely to return false given the user has to be inactive for a large amount of time for this
		// function to be called, but we really should still check.
		if (!model.autosaveReady()) {
			return
		}

		// Check if there's a conflict between what is on the server and what the user is editing.
		//
		// If we were to run save() with a conflict, we'll get a confirmation dialog, and the email won't be remotely
		// saved until the user chooses an option.
		//
		// Since autosaveRemote is being triggered after a very long period of inactivity, the user is almost certainly
		// not present to do that. As such, autosaveRemote() cannot actually autosave remotely.
		if (model.hasDraftDataChangedOnServer()) {
			return
		}

		// The user is currently sending the email, and that is going to save the email for us.
		if (isSending) {
			return
		}

		save(false)
	})

	// This will be invoked while the user is typing.
	const autosaveLocal = throttle(AUTOSAVE_LOCAL_TIMEOUT, async () => {
		// Autosaving should stop working if the dialog is closed.
		if (!dialog.visible) {
			return
		}

		// If the mail was already saved before this was triggered, don't save again.
		if (!model.hasMailChanged()) {
			return
		}

		// The user is currently sending the email, and that is going to save the email for us.
		if (isSending) {
			return
		}

		await model.makeLocalAutosave()
	})

	const send = async () => {
		if (model.isSharedMailbox() && model.containsExternalRecipients() && model.isConfidential()) {
			await Dialog.message("sharedMailboxCanNotSendConfidentialExternal_msg")
			return
		}

		isSending = true
		try {
			// Note: model.send() will save without checking for conflicts, but unlike saving, send() will only ever be
			// triggered by the user, so this is acceptable.
			const success = await model.send(MailMethod.NONE, Dialog.confirm, showProgressDialog)
			if (success) {
				dispose()
				dialog.close()

				const { handleRatingByEvent } = await import("../../../common/ratings/UserSatisfactionDialog.js")
				void handleRatingByEvent("Mail")
			}
		} catch (e) {
			if (e instanceof UserError) {
				showUserError(e)
			} else {
				throw e
			}
		} finally {
			isSending = false
		}
	}

	// keep track of things we need to dispose of when the editor is completely closed
	const disposables: { dispose: () => unknown }[] = []

	const dispose = () => {
		model.dispose()
		if (templatePopupModel) templatePopupModel.dispose()
		for (const disposable of disposables) {
			disposable.dispose()
		}
	}

	const minimize = () => {
		let saveStatus = stream<SaveStatus>({ status: SaveStatusEnum.Saving })
		if (model.hasMailChanged()) {
			save(false)
				.then((status) => saveStatus(status))
				.catch((e) => {
					const reason = isOfflineError(e) ? SaveErrorReason.ConnectionLost : SaveErrorReason.Unknown

					saveStatus({ status: SaveStatusEnum.NotSaved, reason })

					// If we don't show the error in the minimized error dialog,
					// Then we need to communicate it in a dialog or as an unhandled error
					if (reason === SaveErrorReason.Unknown) {
						if (e instanceof UserError) {
							showUserError(e)
						} else {
							throw e
						}
					}
				})
				.finally(() => m.redraw())
		} else if (!model.draft) {
			// If the mail is unchanged and there was no preexisting draft, close instead of saving and return to not show minimized mail editor
			dispose()
			dialog.close()
			return
		} else {
			// If the mail is unchanged and there /is/ a preexisting draft, there was no change and the mail is already saved
			model.clearLocalAutosave()
			saveStatus = stream<SaveStatus>({ status: SaveStatusEnum.Saved })
		}

		if (client.isCalendarApp()) {
			return dialog.close()
		}

		showMinimizedMailEditor(dialog, model, mailLocator.minimizedMailModel, locator.eventController, dispose, saveStatus)
	}

	let windowCloseUnsubscribe = () => {}

	const headerBarAttrs: DialogHeaderBarAttrs = {
		left: [
			{
				label: "close_alt",
				click: () => minimize(),
				type: ButtonType.Secondary,
			},
		],
		right: [
			{
				label: "send_action",
				click: () => {
					send()
				},
				type: ButtonType.Primary,
			},
		],
		middle: dialogTitleTranslationKey(model.getConversationType()),
		create: () => {
			if (isBrowser()) {
				// Have a simple listener on browser, so their browser will make the user ask if they are sure they want to close when closing the tab/window
				windowCloseUnsubscribe = windowFacade.addWindowCloseListener(() => {})
			} else if (isDesktop()) {
				// Simulate clicking the Close button when on the desktop so they can see they can save a draft rather than completely closing it
				windowCloseUnsubscribe = windowFacade.addWindowCloseListener(() => {
					minimize()
				})
			}
		},
		remove: () => {
			windowCloseUnsubscribe()
		},
	}
	const templatePopupModel =
		locator.logins.isInternalUserLoggedIn() && client.isDesktopDevice()
			? new TemplatePopupModel(locator.eventController, locator.logins, locator.entityClient)
			: null

	const createKnowledgebaseButtonAttrs = async (editor: Editor) => {
		if (locator.logins.isInternalUserLoggedIn()) {
			const customer = await locator.logins.getUserController().loadCustomer()
			// only create knowledgebase button for internal users with valid template group and enabled KnowledgebaseFeature
			if (
				styles.isDesktopLayout() &&
				templatePopupModel &&
				locator.logins.getUserController().getTemplateMemberships().length > 0 &&
				isCustomizationEnabledForCustomer(customer, FeatureType.KnowledgeBase)
			) {
				const knowledgebaseModel = new KnowledgeBaseModel(locator.eventController, locator.entityClient, locator.logins.getUserController())
				await knowledgebaseModel.init()

				// make sure we dispose knowledbaseModel once the editor is closed
				disposables.push(knowledgebaseModel)

				const knowledgebaseInjection = createKnowledgeBaseDialogInjection(knowledgebaseModel, templatePopupModel, editor)
				dialog.setInjectionRight(knowledgebaseInjection)
				return knowledgebaseInjection
			} else {
				return null
			}
		} else {
			return null
		}
	}

	mailEditorAttrs = createMailEditorAttrs(
		model,
		blockExternalContent,
		model.toRecipients().length !== 0,
		() => dialog,
		templatePopupModel,
		createKnowledgebaseButtonAttrs,
		await locator.recipientsSearchModel(),
		alwaysBlockExternalContent,
	)

	const shortcuts: Shortcut[] = [
		{
			key: Keys.ESC,
			exec: () => {
				minimize()
			},
			help: "close_alt",
		},
		{
			key: Keys.S,
			ctrlOrCmd: true,
			exec: () => {
				save(true).catch(ofClass(UserError, showUserError))
			},
			help: "save_action",
		},
		{
			key: Keys.S,
			ctrlOrCmd: true,
			shift: true,
			exec: () => {
				send()
			},
			help: "send_action",
		},
		{
			key: Keys.RETURN,
			ctrlOrCmd: true,
			exec: () => {
				send()
			},
			help: "send_action",
		},
	]

	mailEditorAttrs.onChange = () => {
		autosaveLocal()
		autosaveRemote()
	}

	dialog = Dialog.editDialog(headerBarAttrs, MailEditor, mailEditorAttrs)
	dialog.setCloseHandler(() => minimize())

	for (let shortcut of shortcuts) {
		dialog.addShortcut(shortcut)
	}

	return dialog
}

/**
 * open a MailEditor
 * @param mailboxDetails details to use when sending an email
 * @returns {*}
 * @private
 * @throws PermissionError
 */
export async function newMailEditor(mailboxDetails: MailboxDetail): Promise<Dialog | null> {
	// We check approval status so as to get a dialog informing the user that they cannot send mails
	// but we still want to open the mail editor because they should still be able to contact sales@tutao.de
	await checkApprovalStatus(locator.logins, false)
	const { appendEmailSignature } = await import("../signature/Signature")
	const signature = appendEmailSignature("", locator.logins.getUserController().props)
	const detailsProperties = await getMailboxDetailsAndProperties(mailboxDetails)
	return newMailEditorFromTemplate(detailsProperties.mailboxDetails, {}, "", signature)
}

async function getExternalContentRulesForEditor(model: SendMailModel, currentStatus: boolean) {
	let contentRules
	const previousMail = model.getPreviousMail()

	if (!previousMail) {
		contentRules = {
			alwaysBlockExternalContent: false,
			// external content in a mail for which we don't have a
			// previous mail must have been put there by us.
			blockExternalContent: false,
		}
	} else {
		const externalImageRule = await locator.configFacade.getExternalImageRule(previousMail.sender.address).catch((e: unknown) => {
			console.log("Error getting external image rule:", e)
			return ExternalImageRule.None
		})

		const mailDetails = await locator.mailFacade.loadMailDetailsBlob(previousMail)
		const isAuthenticatedMail = mailDetails.authStatus === MailAuthenticationStatus.AUTHENTICATED

		if (externalImageRule === ExternalImageRule.Block || (externalImageRule === ExternalImageRule.None && model.isUserPreviousSender())) {
			contentRules = {
				// When we have an explicit rule for blocking images we don´t
				// want to prompt the user about showing images again
				alwaysBlockExternalContent: externalImageRule === ExternalImageRule.Block,
				blockExternalContent: true,
			}
		} else if (externalImageRule === ExternalImageRule.Allow && isAuthenticatedMail) {
			contentRules = {
				alwaysBlockExternalContent: false,
				blockExternalContent: false,
			}
		} else {
			contentRules = {
				alwaysBlockExternalContent: false,
				blockExternalContent: currentStatus,
			}
		}
	}

	return contentRules
}

export async function newMailEditorAsResponse(
	args: InitAsResponseArgs,
	blockExternalContent: boolean,
	inlineImages: InlineImages,
	mailboxDetails?: MailboxDetail,
): Promise<Dialog | null> {
	if (!(await confirmNewEditor(mailLocator.autosaveFacade, mailLocator.minimizedMailModel))) {
		return null
	}

	const detailsProperties = await getMailboxDetailsAndProperties(mailboxDetails)
	const model = await locator.sendMailModel(detailsProperties.mailboxDetails, detailsProperties.mailboxProperties)
	await model.initAsResponse(args, inlineImages)

	const externalImageRules = await getExternalContentRulesForEditor(model, blockExternalContent)
	return createMailEditorDialog(model, externalImageRules?.blockExternalContent, externalImageRules?.alwaysBlockExternalContent)
}

export async function newMailEditorFromDraft(
	mail: Mail,
	mailDetails: MailDetails,
	conversationEntry: ConversationEntry,
	attachments: TutanotaFile[],
	inlineImages: InlineImages,
	blockExternalContent: boolean,
	localDraftData?: LocalAutosavedDraftData,
	mailboxDetails?: MailboxDetail,
): Promise<Dialog | null> {
	if (localDraftData == null && !(await confirmNewEditor(mailLocator.autosaveFacade, mailLocator.minimizedMailModel))) {
		return null
	}

	const detailsProperties = await getMailboxDetailsAndProperties(mailboxDetails)
	const model = await locator.sendMailModel(detailsProperties.mailboxDetails, detailsProperties.mailboxProperties)
	await model.initWithDraft(mail, mailDetails, conversationEntry, attachments, inlineImages)
	const externalImageRules = await getExternalContentRulesForEditor(model, blockExternalContent)

	if (localDraftData) {
		model.markAsChangedIfNecessary(true)
		model.setWaitUntilSync(true)
		model.setMailRemotelyUpdatedAt(localDraftData.lastUpdatedTime)
		model.setMailSavedAt(localDraftData.editedTime)
		model.setBody(localDraftData.body)
		model.setSender(localDraftData.senderAddress)
		model.setSubject(localDraftData.subject)
		model.setConfidential(localDraftData.confidential)

		for (const to of model.toRecipients()) {
			model.removeRecipient(to, RecipientField.TO)
		}
		for (const cc of model.ccRecipients()) {
			model.removeRecipient(cc, RecipientField.CC)
		}
		for (const bcc of model.bccRecipients()) {
			model.removeRecipient(bcc, RecipientField.BCC)
		}

		await model.addRecipients({ to: localDraftData.to, cc: localDraftData.cc, bcc: localDraftData.bcc })
	}

	return createMailEditorDialog(model, externalImageRules?.blockExternalContent, externalImageRules?.alwaysBlockExternalContent)
}

async function confirmNewEditor(autosaveFacade: AutosaveFacade, minimizedEditorViewModel: MinimizedMailEditorViewModel): Promise<boolean> {
	const data = await autosaveFacade.getAutosavedDraftData()
	if (data == null) {
		return true
	}

	const action: "cancel" | "discard" = await showOverwriteDraftDialog()

	if (action === "discard") {
		// Create a new draft
		await autosaveFacade.clearAutosavedDraftData()
		const existingEditor = data.mailId && minimizedEditorViewModel.getEditorForDraftById(data.mailId)

		if (existingEditor != null) {
			minimizedEditorViewModel.removeMinimizedEditor(existingEditor)
		}
		return true
	}
	return false
}

export async function newMailtoUrlMailEditor(mailtoUrl: string, confidential: boolean, mailboxDetails?: MailboxDetail): Promise<Dialog | null> {
	const detailsProperties = await getMailboxDetailsAndProperties(mailboxDetails)
	const mailTo = parseMailtoUrl(mailtoUrl)
	let dataFiles: Attachment[] = []

	if (mailTo.attach) {
		const attach = mailTo.attach

		if (isDesktop()) {
			const files = await Promise.all(attach.map((uri) => locator.fileApp.readDataFile(uri)))
			dataFiles = files.filter(isNotNull)
		}
		// make sure the user is aware that (and which) files have been attached
		const keepAttachments =
			dataFiles.length === 0 ||
			(await Dialog.confirm("attachmentWarning_msg", "attachFiles_action", () =>
				dataFiles.map((df, i) =>
					m(
						".text-break.selectable.mt-xs",
						{
							title: attach[i],
						},
						df.name,
					),
				),
			))

		if (keepAttachments) {
			const sizeCheckResult = checkAttachmentSize(dataFiles)
			dataFiles = sizeCheckResult.attachableFiles

			if (sizeCheckResult.tooBigFiles.length > 0) {
				await Dialog.message("tooBigAttachment_msg", () => sizeCheckResult.tooBigFiles.map((file) => m(".text-break.selectable", file)))
			}
		} else {
			throw new CancelledError("user cancelled opening mail editor with attachments")
		}
	}

	return newMailEditorFromTemplate(
		detailsProperties.mailboxDetails,
		mailTo.recipients,
		mailTo.subject || "",
		appendEmailSignature(mailTo.body || "", locator.logins.getUserController().props),
		dataFiles,
		confidential,
		undefined,
		true, // emails created with mailto should always save as draft
	)
}

export async function newMailEditorFromTemplate(
	mailboxDetails: MailboxDetail,
	recipients: Recipients,
	subject: string,
	bodyText: string,
	attachments?: ReadonlyArray<Attachment>,
	confidential?: boolean,
	senderMailAddress?: string,
	initialChangedState?: boolean,
): Promise<Dialog | null> {
	if (!(await confirmNewEditor(mailLocator.autosaveFacade, mailLocator.minimizedMailModel))) {
		return null
	}

	const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
	const model = await locator.sendMailModel(mailboxDetails, mailboxProperties)
	await model.initWithTemplate(recipients, subject, bodyText, attachments, confidential, senderMailAddress, initialChangedState)
	return await createMailEditorDialog(model)
}

/**
 * Opens a new mail editor from local draft data.
 *
 * This is called if the mail has not been saved to the server before and thus there is no mail ID yet.
 *
 * @param mailboxModel
 * @param draft
 */
export async function newMailEditorFromLocalDraftData(mailboxModel: MailboxModel, draft: LocalAutosavedDraftData): Promise<Dialog | null> {
	const details = await mailboxModel.getMailboxDetailsForMailGroup(draft.mailGroupId)
	const recipients = {
		to: draft.to,
		cc: draft.cc,
		bcc: draft.bcc,
	}

	const mailboxProperties = await locator.mailboxModel.getMailboxProperties(details.mailboxGroupRoot)
	const model = await locator.sendMailModel(details, mailboxProperties)
	await model.initWithTemplate(recipients, draft.subject, draft.body, [], draft.confidential, draft.senderAddress, true)
	model.markAsChangedIfNecessary(true)
	return await createMailEditorDialog(model)
}

/**
 * Create and show a new mail editor with an invite message
 * @param referralLink
 * @returns {*}
 */
export async function writeInviteMail(referralLink: string) {
	const detailsProperties = await getMailboxDetailsAndProperties(null)
	const username = locator.logins.getUserController().userGroupInfo.name
	const body = lang.get("invitationMailBody_msg", {
		"{registrationLink}": referralLink,
		"{username}": username,
	})
	const { invitationSubject } = await locator.serviceExecutor.get(TranslationService, createTranslationGetIn({ lang: lang.code }))
	const dialog = await newMailEditorFromTemplate(detailsProperties.mailboxDetails, {}, invitationSubject, body, [], false)
	dialog?.show()
}

/**
 * Create and show a new mail editor with an invite message
 * @param link the link to the giftcard
 * @param mailboxDetails
 * @returns {*}
 */
export async function writeGiftCardMail(link: string, mailboxDetails?: MailboxDetail) {
	const detailsProperties = await getMailboxDetailsAndProperties(mailboxDetails)
	const bodyText = lang
		.get("defaultShareGiftCardBody_msg", {
			"{link}": '<a href="' + link + '">' + link + "</a>",
			"{username}": locator.logins.getUserController().userGroupInfo.name,
		})
		.split("\n")
		.join("<br />")
	const { giftCardSubject } = await locator.serviceExecutor.get(TranslationService, createTranslationGetIn({ lang: lang.code }))
	locator
		.sendMailModel(detailsProperties.mailboxDetails, detailsProperties.mailboxProperties)
		.then((model) => model.initWithTemplate({}, giftCardSubject, appendEmailSignature(bodyText, locator.logins.getUserController().props), [], false))
		.then((model) => createMailEditorDialog(model, false))
		.then((dialog) => dialog.show())
}

async function getMailboxDetailsAndProperties(
	mailboxDetails: MailboxDetail | null | undefined,
): Promise<{ mailboxDetails: MailboxDetail; mailboxProperties: MailboxProperties }> {
	mailboxDetails = mailboxDetails ?? (await locator.mailboxModel.getUserMailboxDetails())
	const mailboxProperties = await locator.mailboxModel.getMailboxProperties(mailboxDetails.mailboxGroupRoot)
	return { mailboxDetails, mailboxProperties }
}
