import { SessionStore } from '@backend/auth'
import { Reservation, Settings } from '@backend/domain'
import { SettingsRepository } from '@backend/repository'
import { TZDate } from '@date-fns/tz'
import { buildMapsUrl } from '@core/utils'
import { Injectable, Logger } from '@nestjs/common'
import { randomUUID } from 'node:crypto'
import { format } from 'date-fns'
import { de } from 'date-fns/locale'
import { DASHBOARD_URL, EMAIL_OPTIONS } from '../../../config'
import { EmailService, MailAttachment, MAILER_TEMPLATES, SendMailOptions } from '../../../global'
import { ReservationDayStats, ReservationEmailEvent, ReservationUpdates } from '../types'
import { buildManagementUrl } from '../utils'
import { ICSService } from './ics.service'
import { ReservationStatsService } from './reservation-stats.service'

type ChangeRow = { label: string; oldValue: string; newValue: string }

type EmailContext = Record<string, unknown>

/**
 * Builds and sends all guest and restaurant reservation emails.
 *
 * Every send is wrapped so a failing SES call can never fail the API request
 * that triggered it (see PR #261 for the incident that motivated this).
 */
@Injectable()
export class ReservationEmailService {
	private readonly logger = new Logger(ReservationEmailService.name)

	constructor(
		private readonly _icsService: ICSService,
		private readonly _emailService: EmailService,
		private readonly _settingsRepository: SettingsRepository,
		private readonly _statsService: ReservationStatsService,
	) {}

	/**
	 * Sends the guest and restaurant emails matching the given lifecycle event.
	 * @param event Which lifecycle event happened
	 * @param reservation The reservation after the event
	 * @param updates Optional field-level updates (used to render the change table)
	 */
	async notifyEvent(event: ReservationEmailEvent, reservation: Reservation, updates?: ReservationUpdates): Promise<void> {
		const settings = await this._settingsRepository.findOne({
			select: { customerApp: {}, restaurant: {} },
		})
		if (!settings) {
			this.logger.error({ message: 'Settings not found', tenantId: SessionStore.tenantId })
			return
		}

		const context = this.buildBaseContext(reservation, settings)
		const changes = this.buildChangeRows(updates, settings)
		if (changes.length > 0) context['changes'] = changes

		await this.sendGuestEmail(event, reservation, settings, context)
		await this.sendOwnerEmail(event, reservation, settings, context)
	}

	/**
	 * Send an email to the customer to remind them of reviewing the restaurant
	 */
	async sendReviewEmail(reservation: Reservation): Promise<void> {
		if (!reservation.customerData.email) return

		const settings = await this._settingsRepository.findOne({
			select: { customerApp: {}, restaurant: {} },
		})
		if (!settings) {
			this.logger.error({ message: 'Settings not found', tenantId: SessionStore.tenantId })
			return
		}

		const context = {
			restaurant_name: settings.customerApp.displayName,
			restaurant_logo: settings.customerApp.logoUrl,
			google_place_id: settings.restaurant.googlePlaceId,
		}

		await this.safeSend({
			to: reservation.customerData.email,
			replyTo: {
				address: settings.restaurant.email,
				name: settings.customerApp.displayName,
			},
			subject: `Leave a review for ${settings.customerApp?.displayName}`,
			template: MAILER_TEMPLATES.RESERVATION.REVIEW.MINIMAL,
			context,
		})
	}

	async sendReminderEmail(reservation: Reservation, settings: Settings, restaurantName: string): Promise<void> {
		if (!reservation.customerData.email) return

		const context = this.buildBaseContext(reservation, settings)

		await this.safeSend({
			to: reservation.customerData.email,
			subject: `Erinnerung: Ihre Reservierung – ${restaurantName}`,
			template: MAILER_TEMPLATES.RESERVATION.REMINDER.MINIMAL,
			from: EMAIL_OPTIONS.FROM_EMAIL,
			context,
			text: this.guestPlainText('Erinnerung an Ihre Reservierung', reservation, settings),
		})
	}

	private async sendGuestEmail(
		event: ReservationEmailEvent,
		reservation: Reservation,
		settings: Settings,
		baseContext: EmailContext,
	): Promise<void> {
		const email = reservation.customerData.email
		if (!email) return

		const restaurant = settings.customerApp.displayName

		const byEvent: Partial<
			Record<
				ReservationEmailEvent,
				{ template: string; subject: string; headline: string; withIcs?: boolean; cancelIcs?: boolean }
			>
		> = {
			[ReservationEmailEvent.createdConfirmed]: {
				template: MAILER_TEMPLATES.RESERVATION.GUEST.CONFIRMED,
				subject: `Reservierung bestätigt – ${restaurant}`,
				headline: 'Ihre Reservierung ist bestätigt',
				withIcs: true,
			},
			[ReservationEmailEvent.confirmed]: {
				template: MAILER_TEMPLATES.RESERVATION.GUEST.CONFIRMED,
				subject: `Reservierung bestätigt – ${restaurant}`,
				headline: 'Ihre Reservierung ist bestätigt',
				withIcs: true,
			},
			[ReservationEmailEvent.createdPending]: {
				template: MAILER_TEMPLATES.RESERVATION.GUEST.PENDING,
				subject: `Reservierungsanfrage erhalten – ${restaurant}`,
				headline: 'Ihre Reservierungsanfrage ist eingegangen',
			},
			[ReservationEmailEvent.pending]: {
				template: MAILER_TEMPLATES.RESERVATION.GUEST.PENDING,
				subject: `Ihre Reservierung wird geprüft – ${restaurant}`,
				headline: 'Ihre Reservierung wird geprüft',
			},
			[ReservationEmailEvent.updated]: {
				template: MAILER_TEMPLATES.RESERVATION.GUEST.UPDATED,
				subject: `Ihre Reservierung wurde geändert – ${restaurant}`,
				headline: 'Ihre Reservierung wurde geändert',
				withIcs: true,
			},
			[ReservationEmailEvent.cancelledByGuest]: {
				template: MAILER_TEMPLATES.RESERVATION.GUEST.CANCELLED,
				subject: `Ihre Reservierung wurde storniert – ${restaurant}`,
				headline: 'Ihre Reservierung wurde storniert',
				cancelIcs: true,
			},
			[ReservationEmailEvent.cancelledByRestaurant]: {
				template: MAILER_TEMPLATES.RESERVATION.GUEST.CANCELLED,
				subject: `Ihre Reservierung wurde storniert – ${restaurant}`,
				headline: 'Ihre Reservierung wurde storniert',
				cancelIcs: true,
			},
		}

		const config = byEvent[event]
		if (!config) return

		const attachments: MailAttachment[] = []
		if (config.withIcs) {
			attachments.push({
				filename: 'reservierung.ics',
				content: this._icsService.generateIcs(reservation, settings),
				contentType: 'text/calendar; method=REQUEST',
				cid: randomUUID(),
			})
		} else if (config.cancelIcs) {
			// Same UID with METHOD:CANCEL — calendar apps remove the event
			attachments.push({
				filename: 'reservierung.ics',
				content: this._icsService.generateIcs(reservation, settings, { cancelled: true }),
				contentType: 'text/calendar; method=CANCEL',
				cid: randomUUID(),
			})
		}

		await this.safeSend({
			to: email,
			replyTo: {
				address: settings.restaurant.email,
				name: settings.customerApp.displayName,
			},
			subject: config.subject,
			template: config.template,
			context: { ...baseContext, headline: config.headline },
			...(attachments.length > 0 && { attachments }),
			text: this.guestPlainText(config.headline, reservation, settings),
		})

		// Companions receive the same notification WITHOUT the management link —
		// only the booker may change or cancel the reservation.
		for (const companionEmail of reservation.companionEmails ?? []) {
			await this.safeSend({
				to: companionEmail,
				replyTo: {
					address: settings.restaurant.email,
					name: settings.customerApp.displayName,
				},
				subject: config.subject,
				template: config.template,
				context: { ...baseContext, headline: config.headline, manage_url: undefined },
				...(attachments.length > 0 && { attachments }),
				text: this.guestPlainText(config.headline, reservation, settings, { withManageLink: false }),
			})
		}
	}

	private async sendOwnerEmail(
		event: ReservationEmailEvent,
		reservation: Reservation,
		settings: Settings,
		baseContext: EmailContext,
	): Promise<void> {
		const subjectSuffix = this.ownerSubjectSuffix(reservation, settings)

		const byEvent: Partial<Record<ReservationEmailEvent, { template: string; subject: string; headline: string }>> = {
			[ReservationEmailEvent.createdConfirmed]: {
				template: MAILER_TEMPLATES.RESERVATION.OWNER.NEW,
				subject: `Neue Reservierung · ${subjectSuffix}`,
				headline: 'Neue Reservierung',
			},
			[ReservationEmailEvent.createdPending]: {
				template: MAILER_TEMPLATES.RESERVATION.OWNER.NEW,
				subject: `Neue Reservierungsanfrage · ${subjectSuffix}`,
				headline: 'Neue Reservierungsanfrage',
			},
			[ReservationEmailEvent.updated]: {
				template: MAILER_TEMPLATES.RESERVATION.OWNER.UPDATED,
				subject: `Reservierung geändert · ${subjectSuffix}`,
				headline: 'Reservierung geändert',
			},
			[ReservationEmailEvent.cancelledByGuest]: {
				template: MAILER_TEMPLATES.RESERVATION.OWNER.CANCELLED,
				subject: `Reservierung storniert · ${subjectSuffix}`,
				headline: 'Reservierung storniert',
			},
		}

		const config = byEvent[event]
		if (!config) return

		const timezone = settings.restaurant.timezone
		const dayStats = await this.safeDayStats(reservation, timezone)

		const context: EmailContext = {
			...baseContext,
			headline: config.headline,
			is_pending: event === ReservationEmailEvent.createdPending,
			accept_url: `${DASHBOARD_URL}/reservations?id=${reservation.id}&action=accept`,
			decline_url: `${DASHBOARD_URL}/reservations?id=${reservation.id}&action=decline`,
			dashboard_url: `${DASHBOARD_URL}/reservations?id=${reservation.id}`,
			cancelled_by_guest: event === ReservationEmailEvent.cancelledByGuest,
		}
		if (dayStats) {
			context['day_reservation_count'] = dayStats.reservationCount
			context['day_guest_count'] = dayStats.guestCount
		}

		await this.safeSend({
			to: settings.restaurant.email,
			subject: config.subject,
			template: config.template,
			context,
			text: this.ownerPlainText(config.headline, reservation, settings, dayStats),
		})
	}

	private buildBaseContext(reservation: Reservation, settings: Settings): EmailContext {
		const timezone = settings.restaurant.timezone
		const start = new TZDate(reservation.startDate, timezone)
		const end = new TZDate(reservation.endDate, timezone)

		return {
			restaurant_name: settings.customerApp.displayName,
			restaurant_logo_url: settings.customerApp.logoUrl,
			restaurant_address: settings.restaurant.address,
			restaurant_phone: settings.restaurant.phone,
			restaurant_email: settings.restaurant.email,
			google_place_id: settings.restaurant.googlePlaceId,
			maps_url: buildMapsUrl(settings.restaurant.googlePlaceId, settings.restaurant.address),
			date_long: format(start, 'EEEE, d. MMMM yyyy', { locale: de }),
			date_short: format(start, 'EEEEEE. dd.MM.', { locale: de }),
			time_start: format(start, 'HH:mm', { locale: de }),
			time_range: `${format(start, 'HH:mm', { locale: de })}–${format(end, 'HH:mm', { locale: de })} Uhr`,
			persons_label: this.personsLabel(reservation.noOfPersons),
			guest_name: reservation.customerData.name,
			guest_email: reservation.customerData.email,
			guest_phone: reservation.customerData.phoneNumber,
			comment: reservation.comment,
			manage_url: this.managementUrl(reservation),
		}
	}

	/**
	 * Renders field updates as labeled old→new rows. Only guest-visible fields
	 * are shown; internal fields (state, tables, tokens) are skipped.
	 */
	private buildChangeRows(updates: ReservationUpdates | undefined, settings: Settings): ChangeRow[] {
		if (!updates) return []

		const timezone = settings.restaurant.timezone
		const rows: ChangeRow[] = []

		for (const update of updates) {
			if (!update) continue

			switch (update.field) {
				case 'startDate': {
					const oldDate = new TZDate(update.oldValue as Date, timezone)
					const newDate = new TZDate(update.newValue as Date, timezone)
					const oldDay = format(oldDate, 'EEEEEE. dd.MM.', { locale: de })
					const newDay = format(newDate, 'EEEEEE. dd.MM.', { locale: de })
					if (oldDay !== newDay) {
						rows.push({ label: 'Datum', oldValue: oldDay, newValue: newDay })
					}
					const oldTime = format(oldDate, 'HH:mm', { locale: de })
					const newTime = format(newDate, 'HH:mm', { locale: de })
					if (oldTime !== newTime) {
						rows.push({ label: 'Uhrzeit', oldValue: `${oldTime} Uhr`, newValue: `${newTime} Uhr` })
					}
					break
				}
				case 'noOfPersons': {
					rows.push({
						label: 'Personen',
						oldValue: this.personsLabel(update.oldValue as number),
						newValue: this.personsLabel(update.newValue as number),
					})
					break
				}
				case 'comment': {
					rows.push({
						label: 'Hinweis',
						oldValue: (update.oldValue as string) || '—',
						newValue: (update.newValue as string) || '—',
					})
					break
				}
				case 'customerData': {
					const oldData = update.oldValue as Reservation['customerData'] | undefined
					const newData = update.newValue as Reservation['customerData'] | undefined
					if (!oldData || !newData) break
					if (oldData.name !== newData.name) {
						rows.push({ label: 'Name', oldValue: oldData.name, newValue: newData.name })
					}
					if (oldData.email !== newData.email) {
						rows.push({ label: 'E-Mail', oldValue: oldData.email || '—', newValue: newData.email || '—' })
					}
					if (oldData.phoneNumber !== newData.phoneNumber) {
						rows.push({ label: 'Telefon', oldValue: oldData.phoneNumber || '—', newValue: newData.phoneNumber || '—' })
					}
					break
				}
			}
		}

		return rows
	}

	private ownerSubjectSuffix(reservation: Reservation, settings: Settings): string {
		const timezone = settings.restaurant.timezone
		const start = new TZDate(reservation.startDate, timezone)
		const name = reservation.customerData.lastName || reservation.customerData.name
		return `${this.personsLabel(reservation.noOfPersons)} · ${format(start, 'EEEEEE. dd.MM.', { locale: de })} · ${format(start, 'HH:mm', { locale: de })} · ${name}`
	}

	private guestPlainText(
		headline: string,
		reservation: Reservation,
		settings: Settings,
		options?: { withManageLink?: boolean },
	): string {
		const timezone = settings.restaurant.timezone
		const start = new TZDate(reservation.startDate, timezone)
		const lines = [
			headline,
			'',
			settings.customerApp.displayName,
			format(start, 'EEEE, d. MMMM yyyy', { locale: de }),
			`${format(start, 'HH:mm', { locale: de })} Uhr`,
			this.personsLabel(reservation.noOfPersons),
		]
		if (reservation.comment) lines.push(`Hinweis: ${reservation.comment}`)
		const manageUrl = options?.withManageLink === false ? undefined : this.managementUrl(reservation)
		if (manageUrl) {
			lines.push('', `Reservierung verwalten: ${manageUrl}`)
		}
		if (settings.restaurant.phone) lines.push(`Telefon: ${settings.restaurant.phone}`)
		if (settings.restaurant.address) lines.push(settings.restaurant.address)
		return lines.join('\n')
	}

	private ownerPlainText(
		headline: string,
		reservation: Reservation,
		settings: Settings,
		dayStats?: ReservationDayStats,
	): string {
		const timezone = settings.restaurant.timezone
		const start = new TZDate(reservation.startDate, timezone)
		const lines = [
			headline,
			'',
			`${format(start, 'EEEE, d. MMMM yyyy', { locale: de })} · ${format(start, 'HH:mm', { locale: de })} Uhr`,
			this.personsLabel(reservation.noOfPersons),
			reservation.customerData.name,
		]
		if (reservation.customerData.phoneNumber) lines.push(`Telefon: ${reservation.customerData.phoneNumber}`)
		if (reservation.customerData.email) lines.push(`E-Mail: ${reservation.customerData.email}`)
		if (reservation.comment) lines.push(`Kommentar: ${reservation.comment}`)
		if (dayStats) {
			lines.push(
				'',
				`Reservierungsstand für ${format(start, 'EEEE, d. MMMM', { locale: de })}: ${dayStats.reservationCount} Reservierungen · ${dayStats.guestCount} Personen`,
			)
		}
		return lines.join('\n')
	}

	private personsLabel(noOfPersons: number): string {
		return noOfPersons === 1 ? '1 Person' : `${noOfPersons} Personen`
	}

	private managementUrl(reservation: Reservation): string | undefined {
		return buildManagementUrl(reservation.managementToken)
	}

	private async safeDayStats(reservation: Reservation, timezone: string): Promise<ReservationDayStats | undefined> {
		try {
			return await this._statsService.getDayStats(reservation.startDate, timezone)
		} catch (error) {
			this.logger.error({
				message: 'Failed to compute day stats for owner email',
				reservationId: reservation.id,
				error: error instanceof Error ? error.message : error,
			})
			return undefined
		}
	}

	/** Sends the mail and logs failures instead of throwing. */
	private async safeSend(options: SendMailOptions): Promise<void> {
		try {
			await this._emailService.send(options)
		} catch (error) {
			this.logger.error({
				message: 'Failed to send reservation email',
				to: options.to,
				subject: options.subject,
				template: options.template,
				error: error instanceof Error ? error.message : error,
			})
		}
	}
}
