import { createHash } from 'crypto'
import { zipSync } from 'fflate'
import { promises as fs } from 'fs'
import { Boom } from '@hapi/boom'
import { proto } from '../../WAProto/index.js'
import type { MediaType } from '../Defaults/index.js'
import type { Sticker, StickerPack, WAMediaUpload, WAMediaUploadFunction } from '../Types/Message.js'
import type { ILogger } from './logger.js'
import { generateMessageIDV2 } from './generics.js'
import { getImageProcessingLibrary, encryptedStream } from './messages-media.js'

/**
 * Verifica se um buffer é um arquivo WebP válido
 * Valida os magic bytes: RIFF....WEBP
 *
 * @param buffer - Buffer to check
 * @returns true if buffer is valid WebP format
 *
 * @example
 * ```typescript
 * const buffer = await readFile('image.webp')
 * if (isWebPBuffer(buffer)) {
 *   console.log('Valid WebP file')
 * }
 * ```
 */
export const isWebPBuffer = (buffer: Buffer): boolean => {
	if (buffer.length < 12) return false

	// Verifica magic bytes RIFF (0-3) e WEBP (8-11)
	const riffHeader = buffer.toString('ascii', 0, 4)
	const webpHeader = buffer.toString('ascii', 8, 12)

	return riffHeader === 'RIFF' && webpHeader === 'WEBP'
}

/**
 * Detecta se um WebP é animado através da análise de chunks
 *
 * Analisa a estrutura do arquivo WebP procurando por:
 * - VP8X header com animation flag (bit 1)
 * - Chunks ANIM (animation) ou ANMF (animation frame)
 *
 * SECURITY: Implements robust validation to prevent:
 * - Integer overflow attacks (malicious chunk sizes)
 * - Out-of-bounds reads (buffer overflow)
 * - Infinite loop DoS (iteration limit)
 *
 * @param buffer - WebP buffer to analyze
 * @returns true if WebP is animated, false if static or malformed
 *
 * @example
 * ```typescript
 * const webpBuffer = await readFile('sticker.webp')
 * if (isAnimatedWebP(webpBuffer)) {
 *   console.log('Animated sticker detected')
 * }
 * ```
 */
export const isAnimatedWebP = (buffer: Buffer): boolean => {
	if (!isWebPBuffer(buffer)) return false

	const MAX_CHUNK_SIZE = 100 * 1024 * 1024 // 100MB max per chunk
	const MAX_ITERATIONS = 1000 // Prevent infinite loop

	let offset = 12 // Skip RIFF header (12 bytes)
	let iterations = 0

	while (offset < buffer.length - 8 && iterations++ < MAX_ITERATIONS) {
		const chunkFourCC = buffer.toString('ascii', offset, offset + 4)
		const chunkSize = buffer.readUInt32LE(offset + 4)

		// SECURITY: Validate chunk size to prevent integer overflow and buffer overflow
		if (chunkSize < 0 || chunkSize > MAX_CHUNK_SIZE) {
			// Invalid chunk size - treat as non-animated
			return false
		}

		// SECURITY: Verify chunk fits within buffer bounds
		if (offset + 8 + chunkSize > buffer.length) {
			// Chunk extends beyond buffer - malformed file
			return false
		}

		// VP8X extended header - check animation flag
		if (chunkFourCC === 'VP8X' && offset + 8 < buffer.length) {
			const flags = buffer[offset + 8]
			// Bit 1 (0x02) = animation flag
			if (flags && (flags & 0x02)) return true
		}

		// Animation chunks
		if (chunkFourCC === 'ANIM' || chunkFourCC === 'ANMF') {
			return true
		}

		// Move to next chunk (8 byte header + chunk size + padding)
		offset += 8 + chunkSize + (chunkSize % 2)
	}

	return false
}

/**
 * Converte uma imagem para WebP usando Sharp
 * Preserva o buffer original se já for WebP para manter EXIF e animações
 *
 * @param buffer - Image buffer to convert
 * @param logger - Optional logger for debugging
 * @returns Object with WebP buffer and animation status
 *
 * @throws {Boom} If Sharp is not installed and buffer is not WebP
 */
const convertToWebP = async (
	buffer: Buffer,
	logger?: ILogger
): Promise<{ webpBuffer: Buffer; isAnimated: boolean }> => {
	// Se já é WebP, preserva o buffer original (mantém EXIF e animações)
	if (isWebPBuffer(buffer)) {
		const isAnimated = isAnimatedWebP(buffer)
		logger?.trace({ isAnimated }, 'Input is already WebP, preserving original buffer')
		return { webpBuffer: buffer, isAnimated }
	}

	// Tenta usar Sharp para converter
	const lib = await getImageProcessingLibrary()

	if (!lib?.sharp) {
		throw new Boom(
			'Sharp library is required to convert non-WebP images to WebP format. Install with: yarn add sharp',
			{ statusCode: 400 }
		)
	}

	logger?.trace('Converting image to WebP using Sharp')
	const webpBuffer = await lib.sharp.default(buffer).webp().toBuffer()

	return { webpBuffer, isAnimated: false }
}

/**
 * Gera hash SHA256 em formato base64 URL-safe (RFC 4648)
 * Usado para nomear arquivos de stickers no ZIP (auto-deduplicação)
 *
 * SECURITY: Correctly implements base64url encoding to prevent hash collisions:
 * - '+' → '-'
 * - '/' → '_' (DIFFERENT from '+' mapping)
 * - '=' padding removed
 *
 * @param buffer - Buffer to hash
 * @returns Base64 URL-safe SHA256 hash (RFC 4648 compliant)
 */
const generateSha256Hash = (buffer: Buffer): string => {
	return createHash('sha256')
		.update(buffer)
		.digest('base64')
		.replace(/\+/g, '-') // + becomes -
		.replace(/\//g, '_') // / becomes _ (CRITICAL: different from + mapping!)
		.replace(/=/g, '')   // Remove padding
}

/**
 * Converte WAMediaUpload para Buffer com limites de segurança
 * Suporta Buffer, Stream, URL e Data URLs
 *
 * SECURITY: Implements protections against:
 * - Memory exhaustion (size limits)
 * - Slow read attacks (timeouts)
 * - Resource DoS (stream cleanup)
 *
 * @param media - Media input (Buffer, Stream, URL or Data URL)
 * @param context - Context for error messages (e.g., 'sticker', 'cover')
 * @param options - Optional size limit and timeout
 * @returns Buffer with media content
 *
 * @throws {Boom} If media format is invalid, too large, or timeout
 */
const mediaToBuffer = async (
	media: WAMediaUpload,
	context: string,
	options?: { maxSize?: number; timeout?: number }
): Promise<Buffer> => {
	const MAX_SIZE = options?.maxSize || 10 * 1024 * 1024 // 10MB default
	const TIMEOUT = options?.timeout || 30000 // 30s default

	if (Buffer.isBuffer(media)) {
		// SECURITY: Validate buffer size
		if (media.length > MAX_SIZE) {
			throw new Boom(`${context} size (${(media.length / 1024).toFixed(2)}KB) exceeds ${MAX_SIZE / 1024}KB limit`, {
				statusCode: 413
			})
		}
		return media
	} else if (typeof media === 'object' && 'url' in media) {
		const url = media.url.toString()

		// ENHANCEMENT: Support Data URLs (data:image/...)
		if (url.startsWith('data:')) {
			try {
				const base64Data = url.split(',')[1]
				if (!base64Data) {
					throw new Boom(`Invalid data URL for ${context}: missing base64 data`, { statusCode: 400 })
				}
				const buffer = Buffer.from(base64Data, 'base64')

				// SECURITY: Validate buffer size
				if (buffer.length > MAX_SIZE) {
					throw new Boom(
						`${context} data URL size (${(buffer.length / 1024).toFixed(2)}KB) exceeds ${MAX_SIZE / 1024}KB limit`,
						{ statusCode: 413 }
					)
				}

				return buffer
			} catch (error) {
				if (error instanceof Boom) throw error
				throw new Boom(`Failed to parse data URL for ${context}: ${(error as Error).message}`, {
					statusCode: 400
				})
			}
		}

		// HTTP/HTTPS URLs - download with size limit and timeout
		const controller = new AbortController()
		const timeoutId = setTimeout(() => controller.abort(), TIMEOUT)

		try {
			const response = await fetch(url, {
				signal: controller.signal
			})

			if (!response.ok) {
				throw new Boom(`Failed to download ${context} from URL: ${url}`, {
					statusCode: 400,
					data: { url, status: response.status }
				})
			}

			// SECURITY: Check Content-Length header before downloading
			const contentLength = response.headers.get('content-length')
			if (contentLength && parseInt(contentLength) > MAX_SIZE) {
				throw new Boom(
					`${context} URL file size (${(parseInt(contentLength) / 1024).toFixed(2)}KB) exceeds ${MAX_SIZE / 1024}KB limit`,
					{ statusCode: 413, data: { url, contentLength } }
				)
			}

			// SECURITY: Stream download with size validation
			const chunks: Buffer[] = []
			let totalSize = 0

			if (!response.body) {
				throw new Boom(`${context} URL response has no body`, { statusCode: 400, data: { url } })
			}

			for await (const chunk of response.body as any) {
				const buffer = Buffer.from(chunk)
				totalSize += buffer.length

				// SECURITY: Enforce size limit during download
				if (totalSize > MAX_SIZE) {
					throw new Boom(
						`${context} download (${(totalSize / 1024).toFixed(2)}KB) exceeded ${MAX_SIZE / 1024}KB limit`,
						{ statusCode: 413, data: { url } }
					)
				}

				chunks.push(buffer)
			}

			return Buffer.concat(chunks)
		} finally {
			clearTimeout(timeoutId)
		}
	} else if (typeof media === 'object' && 'stream' in media) {
		// SECURITY: Read stream with size limit and timeout
		const chunks: Buffer[] = []
		let totalSize = 0

		const timeoutPromise = new Promise<never>((_, reject) =>
			setTimeout(() => reject(new Boom(`${context} stream timeout after ${TIMEOUT}ms`, { statusCode: 408 })), TIMEOUT)
		)

		try {
			await Promise.race([
				(async () => {
					for await (const chunk of media.stream) {
						const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
						totalSize += buffer.length

						// SECURITY: Check size limit
						if (totalSize > MAX_SIZE) {
							throw new Boom(
								`${context} stream size (${(totalSize / 1024).toFixed(2)}KB) exceeds ${MAX_SIZE / 1024}KB limit`,
								{ statusCode: 413 }
							)
						}

						chunks.push(buffer)
					}
				})(),
				timeoutPromise
			])

			return Buffer.concat(chunks)
		} catch (error) {
			// SECURITY: Cleanup on error
			media.stream.destroy()
			throw error
		}
	} else {
		throw new Boom(`Invalid ${context} data format`, { statusCode: 400 })
	}
}

export type PrepareStickerPackMessageOptions = {
	/** Upload function to encrypt and upload media to WhatsApp servers */
	upload: WAMediaUploadFunction
	/** Optional logger for debugging */
	logger?: ILogger
	/** Timeout for media uploads */
	mediaUploadTimeoutMs?: number
}

/**
 * Prepara uma mensagem de sticker pack para envio
 *
 * **Processo:**
 * 1. Valida número de stickers (3-30 conforme padrão WhatsApp oficial)
 * 2. Processa cada sticker (converte para WebP se necessário)
 * 3. Cria ZIP com stickers + cover (deduplicação automática por hash)
 * 4. Criptografa ZIP usando AES-256-CBC + HMAC-SHA256
 * 5. Gera thumbnail da capa (252x252 JPEG)
 * 6. Faz upload do ZIP e thumbnail (reutiliza mesma mediaKey)
 * 7. Retorna proto.Message.StickerPackMessage completo
 *
 * **Especificações WhatsApp:**
 * - 3-30 stickers por pack (oficial)
 * - WebP obrigatório
 * - Recomendado: 100KB por sticker estático, 500KB animado
 * - Tray icon: 252x252 pixels
 *
 * @param stickerPack - Sticker pack data with stickers, cover, name, publisher
 * @param options - Upload function and optional logger
 * @returns Prepared StickerPackMessage ready to send
 *
 * @throws {Boom} If validation fails (sticker count, size limits, format issues)
 *
 * @example
 * ```typescript
 * const stickerPackMessage = await prepareStickerPackMessage(
 *   {
 *     name: 'My Pack',
 *     publisher: 'Author',
 *     cover: coverBuffer,
 *     stickers: [
 *       { data: sticker1Buffer, emojis: ['😀'] },
 *       { data: sticker2Buffer, emojis: ['😎'] }
 *     ]
 *   },
 *   { upload: uploadFunction, logger }
 * )
 * ```
 */
export const prepareStickerPackMessage = async (
	stickerPack: StickerPack,
	options: PrepareStickerPackMessageOptions
): Promise<proto.Message.StickerPackMessage> => {
	const { upload, logger, mediaUploadTimeoutMs } = options
	const { stickers, cover, name, publisher, description, packId } = stickerPack

	// Helper function to encrypt and upload media with guaranteed cleanup
	// SECURITY FIX #5: Try/finally ensures temp file cleanup even on upload failure
	const uploadMedia = async (buffer: Buffer, mediaType: MediaType, opts?: { mediaKey?: Uint8Array }) => {
		let encFilePath: string | undefined

		try {
			// Encrypt the media
			const encrypted = await encryptedStream(buffer, mediaType, {
				logger,
				mediaKey: opts?.mediaKey
			})

			encFilePath = encrypted.encFilePath

			// Upload encrypted file
			const result = await upload(encrypted.encFilePath, {
				fileEncSha256B64: encrypted.fileEncSha256.toString('base64'),
				mediaType,
				timeoutMs: mediaUploadTimeoutMs
			})

			return {
				mediaKey: encrypted.mediaKey,
				fileSha256: encrypted.fileSha256,
				fileEncSha256: encrypted.fileEncSha256,
				directPath: result.directPath,
				mediaKeyTimestamp: result.ts
			}
		} finally {
			// SECURITY: Always cleanup temp file, even on error
			if (encFilePath) {
				try {
					await fs.unlink(encFilePath)
					logger?.trace({ encFilePath }, 'Cleaned up temporary encrypted file')
				} catch (unlinkError) {
					// Log but don't fail - file may not exist
					logger?.warn({ encFilePath, error: unlinkError }, 'Failed to cleanup temp file')
				}
			}
		}
	}

	// 1. Validações - Padrão WhatsApp oficial: 3-30 stickers
	// SECURITY FIX #4: Validate actual valid stickers (not undefined/null)
	const validStickers = stickers.filter((s): s is NonNullable<typeof s> => s !== null && s !== undefined)

	if (validStickers.length < 3 || validStickers.length > 30) {
		throw new Boom(
			`Sticker pack must contain between 3 and 30 valid stickers per WhatsApp official spec. ` +
				`Provided: ${validStickers.length} valid stickers ` +
				`(${stickers.length} total, ${stickers.length - validStickers.length} invalid/undefined)`,
			{ statusCode: 400 }
		)
	}

	// Validação de nomes (max 128 caracteres)
	if (name.length > 128) {
		throw new Boom(`Pack name must be 128 characters or less. Current length: ${name.length}`, {
			statusCode: 400
		})
	}

	if (publisher.length > 128) {
		throw new Boom(`Publisher name must be 128 characters or less. Current length: ${publisher.length}`, {
			statusCode: 400
		})
	}

	logger?.info({ stickerCount: stickers.length, name, publisher }, 'Preparing sticker pack message')

	// 2. Gera ID do pack se não fornecido
	const stickerPackId = packId || generateMessageIDV2()

	// 3. Processa stickers e cria estrutura ZIP
	// SECURITY FIX #6: Parallel processing for better performance (30 stickers = significant speedup)
	// SECURITY FIX #7: Track deduplication to merge metadata correctly
	const stickerData: Record<string, [Uint8Array, { level: 0 }]> = {}
	const stickerMetadata: proto.Message.StickerPackMessage.ISticker[] = []
	// Track metadata by hash to merge duplicates
	const metadataByHash = new Map<string, proto.Message.StickerPackMessage.ISticker>()

	// Process all stickers in parallel for performance
	const processedStickers = await Promise.all(
		stickers.map(async (sticker, i) => {
			if (!sticker) return null // Skip undefined stickers

			// SECURITY FIX #8: Better error context for debugging
			try {
				logger?.trace({ index: i }, 'Processing sticker')

				// Obtém buffer do sticker
				const buffer = await mediaToBuffer(sticker.data, `sticker ${i + 1}`)

				// Converte para WebP
				let { webpBuffer, isAnimated } = await convertToWebP(buffer, logger)

				// ENHANCEMENT: Auto-compression if exceeds 1MB (try quality 70, then 50)
				const MAX_STICKER_SIZE = 1024 * 1024 // 1MB
				const recommendedLimit = isAnimated ? 500 : 100

				if (webpBuffer.length > MAX_STICKER_SIZE) {
					logger?.warn(
						{ index: i, sizeKB: (webpBuffer.length / 1024).toFixed(2) },
						`Sticker ${i + 1} exceeds 1MB, attempting compression...`
					)

					const lib = await getImageProcessingLibrary()

					if (lib?.sharp) {
						// Try quality 70
						try {
							const compressed70 = await lib.sharp.default(buffer).webp({ quality: 70 }).toBuffer()

							if (compressed70.length <= MAX_STICKER_SIZE) {
								webpBuffer = compressed70
								logger?.info(
									{ index: i, originalKB: (webpBuffer.length / 1024).toFixed(2), compressedKB: (compressed70.length / 1024).toFixed(2) },
									`Sticker ${i + 1} compressed successfully (quality 70)`
								)
							} else {
								// Try quality 50
								const compressed50 = await lib.sharp.default(buffer).webp({ quality: 50 }).toBuffer()

								if (compressed50.length <= MAX_STICKER_SIZE) {
									webpBuffer = compressed50
									logger?.info(
										{ index: i, originalKB: (webpBuffer.length / 1024).toFixed(2), compressedKB: (compressed50.length / 1024).toFixed(2) },
										`Sticker ${i + 1} compressed successfully (quality 50)`
									)
								} else {
									// Still too large
									throw new Boom(
										`Sticker ${i + 1} still exceeds 1MB after compression (${(compressed50.length / 1024).toFixed(2)}KB). ` +
											`Please use a smaller image.`,
										{ statusCode: 400 }
									)
								}
							}
						} catch (compressionError) {
							// If compression fails, throw error about size
							throw new Boom(
								`Sticker ${i + 1} exceeds 1MB and compression failed: ${(compressionError as Error).message}`,
								{ statusCode: 400 }
							)
						}
					} else {
						// No Sharp available, can't compress
						throw new Boom(
							`Sticker ${i + 1} exceeds the 1MB hard limit (${(webpBuffer.length / 1024).toFixed(2)}KB). ` +
								`Sharp library required for auto-compression. Install with: yarn add sharp`,
							{ statusCode: 400 }
						)
					}
				}

				// Check recommended size (warning only)
				const finalSizeKB = webpBuffer.length / 1024
				if (finalSizeKB > recommendedLimit) {
					logger?.warn(
						{ index: i, sizeKB: finalSizeKB, recommendedLimit, isAnimated },
						`Sticker ${i + 1} exceeds WhatsApp recommended size (${recommendedLimit}KB). ` +
							`This may cause slower sending or delivery issues.`
					)
				}

				// Gera nome do arquivo: hash.webp (deduplicação automática)
				const sha256Hash = generateSha256Hash(webpBuffer)
				const fileName = `${sha256Hash}.webp`

				logger?.trace(
					{ index: i, fileName, sizeKB: finalSizeKB.toFixed(2), isAnimated },
					'Sticker processed successfully'
				)

				return {
					fileName,
					webpBuffer,
					isAnimated,
					emojis: sticker.emojis || [],
					accessibilityLabel: sticker.accessibilityLabel
				}
			} catch (error) {
				// SECURITY FIX #8: Wrap errors with sticker context
				throw new Boom(`Failed to process sticker ${i + 1}: ${(error as Error).message}`, {
					statusCode: error instanceof Boom ? error.output.statusCode : 500,
					data: { stickerIndex: i, originalError: error }
				})
			}
		})
	)

	// Build stickerData and merge metadata for duplicates
	let duplicateCount = 0
	for (const result of processedStickers) {
		if (!result) continue

		const { fileName, webpBuffer, isAnimated, emojis, accessibilityLabel } = result

		// SECURITY FIX #7: Check if this hash already exists (duplicate sticker)
		const existingMetadata = metadataByHash.get(fileName)

		if (existingMetadata) {
			// Duplicate detected - merge metadata (combine emojis and labels)
			duplicateCount++

			// Merge emojis (deduplicate)
			const mergedEmojis = Array.from(new Set([...existingMetadata.emojis!, ...emojis]))
			existingMetadata.emojis = mergedEmojis

			// Merge accessibility labels (concatenate with separator if both exist)
			if (accessibilityLabel) {
				if (existingMetadata.accessibilityLabel) {
					existingMetadata.accessibilityLabel += ` / ${accessibilityLabel}`
				} else {
					existingMetadata.accessibilityLabel = accessibilityLabel
				}
			}

			logger?.debug(
				{ fileName, mergedEmojis, duplicateCount },
				'Duplicate sticker detected - merged metadata'
			)
		} else {
			// New sticker - add to ZIP and create metadata
			stickerData[fileName] = [new Uint8Array(webpBuffer), { level: 0 as 0 }]

			const metadata: proto.Message.StickerPackMessage.ISticker = {
				fileName,
				isAnimated,
				emojis,
				accessibilityLabel,
				isLottie: false,
				mimetype: 'image/webp'
			}

			metadataByHash.set(fileName, metadata)
			stickerMetadata.push(metadata)
		}
	}

	if (duplicateCount > 0) {
		logger?.info(
			{ duplicateCount, uniqueStickers: stickerMetadata.length },
			`Removed ${duplicateCount} duplicate stickers via deduplication`
		)
	}

	// 4. Processa cover image (tray icon)
	// SECURITY FIX #8: Error context for cover processing
	let coverBuffer: Buffer
	let coverWebP: Buffer
	let coverFileName: string

	try {
		logger?.trace('Processing cover image')
		coverBuffer = await mediaToBuffer(cover, 'cover image')

		// Converte cover para WebP e adiciona ao ZIP
		const result = await convertToWebP(coverBuffer, logger)
		coverWebP = result.webpBuffer
		coverFileName = `${stickerPackId}.webp`
		stickerData[coverFileName] = [new Uint8Array(coverWebP), { level: 0 as 0 }]
	} catch (error) {
		throw new Boom(`Failed to process cover image: ${(error as Error).message}`, {
			statusCode: error instanceof Boom ? error.output.statusCode : 500,
			data: { originalError: error }
		})
	}

	// 5. Cria ZIP (level 0 = sem compressão para velocidade)
	// SECURITY FIX #8: Error context for ZIP creation
	let zipBuffer: Buffer
	let uniqueFiles: number

	try {
		uniqueFiles = Object.keys(stickerData).length
		logger?.trace({ totalFiles: uniqueFiles, includingCover: true }, 'Creating ZIP file')

		zipBuffer = Buffer.from(zipSync(stickerData))

		logger?.info({ zipSizeKB: (zipBuffer.length / 1024).toFixed(2) }, 'ZIP file created successfully')

		// Validação de tamanho total (30MB limit para segurança)
		const MAX_PACK_SIZE = 30 * 1024 * 1024
		if (zipBuffer.length > MAX_PACK_SIZE) {
			throw new Boom(
				`Total pack size exceeds ${MAX_PACK_SIZE / 1024 / 1024}MB limit. ` +
					`Current size: ${(zipBuffer.length / 1024 / 1024).toFixed(2)}MB. ` +
					`Try compressing stickers or reducing pack size.`,
				{ statusCode: 400 }
			)
		}
	} catch (error) {
		throw new Boom(`Failed to create ZIP archive: ${(error as Error).message}`, {
			statusCode: error instanceof Boom ? error.output.statusCode : 500,
			data: { originalError: error }
		})
	}

	// 6. Upload do ZIP criptografado
	// SECURITY FIX #8: Error context for sticker pack upload
	let stickerPackUpload: Awaited<ReturnType<typeof uploadMedia>>

	try {
		logger?.trace('Uploading encrypted sticker pack ZIP')
		stickerPackUpload = await uploadMedia(zipBuffer, 'sticker-pack')
	} catch (error) {
		throw new Boom(`Failed to upload sticker pack: ${(error as Error).message}`, {
			statusCode: error instanceof Boom ? error.output.statusCode : 500,
			data: { originalError: error }
		})
	}

	// 7. Gera thumbnail 252x252 JPEG
	// SECURITY FIX #8: Error context for thumbnail generation
	let thumbnailBuffer: Buffer

	try {
		logger?.trace('Generating thumbnail (252x252 JPEG)')
		const lib = await getImageProcessingLibrary()

		if (!lib?.sharp) {
			throw new Boom(
				'Sharp library is required for thumbnail generation. Install with: yarn add sharp',
				{ statusCode: 400 }
			)
		}

		thumbnailBuffer = await lib.sharp
			.default(coverBuffer)
			.resize(252, 252, { fit: 'cover', position: 'center' })
			.jpeg({ quality: 85 })
			.toBuffer()

		logger?.trace({ thumbnailSizeKB: (thumbnailBuffer.length / 1024).toFixed(2) }, 'Thumbnail generated')
	} catch (error) {
		throw new Boom(`Failed to generate thumbnail: ${(error as Error).message}`, {
			statusCode: error instanceof Boom ? error.output.statusCode : 500,
			data: { originalError: error }
		})
	}

	// 8. Upload do thumbnail (REUTILIZA mesma mediaKey - requerido pelo protocolo!)
	// SECURITY FIX #8: Error context for thumbnail upload
	let thumbUpload: Awaited<ReturnType<typeof uploadMedia>>

	try {
		logger?.trace('Uploading thumbnail with same mediaKey')
		thumbUpload = await uploadMedia(thumbnailBuffer, 'thumbnail-sticker-pack', {
			mediaKey: stickerPackUpload.mediaKey // CRÍTICO: mesma chave!
		})
	} catch (error) {
		throw new Boom(`Failed to upload thumbnail: ${(error as Error).message}`, {
			statusCode: error instanceof Boom ? error.output.statusCode : 500,
			data: { originalError: error }
		})
	}

	// 9. Monta mensagem protobuf
	logger?.info(
		{
			packId: stickerPackId,
			totalStickers: stickers.length,
			uniqueFiles: uniqueFiles - 1, // minus cover
			zipSizeKB: (zipBuffer.length / 1024).toFixed(2)
		},
		'Sticker pack message prepared successfully'
	)

	return proto.Message.StickerPackMessage.create({
		// Metadata do pack
		stickerPackId,
		name,
		publisher,
		packDescription: description,
		stickerPackOrigin: proto.Message.StickerPackMessage.StickerPackOrigin.USER_CREATED,
		stickerPackSize: zipBuffer.length,
		stickers: stickerMetadata,

		// ZIP file (criptografado)
		fileSha256: stickerPackUpload.fileSha256,
		fileEncSha256: stickerPackUpload.fileEncSha256,
		mediaKey: stickerPackUpload.mediaKey,
		directPath: stickerPackUpload.directPath,
		fileLength: zipBuffer.length,
		mediaKeyTimestamp: stickerPackUpload.mediaKeyTimestamp,

		// Tray icon info
		trayIconFileName: coverFileName,

		// Thumbnail (criptografado com mesma key)
		thumbnailDirectPath: thumbUpload.directPath,
		thumbnailSha256: createHash('sha256').update(thumbnailBuffer).digest(),
		thumbnailEncSha256: thumbUpload.fileEncSha256,
		thumbnailHeight: 252,
		thumbnailWidth: 252,
		imageDataHash: createHash('sha256').update(thumbnailBuffer).digest('base64')
	})
}
