import type { AbstractWebNotificationManager } from '@modrinth/ui'
import type { JWTAuth, ModuleError, ModuleName } from '@modrinth/utils'
import { ModrinthServerError } from '@modrinth/utils'

import { ContentModule, GeneralModule, NetworkModule, StartupModule } from './modules/index.ts'
import { useServersFetch } from './servers-fetch.ts'

export function handleServersError(err: any, notifications: AbstractWebNotificationManager) {
	if (err instanceof ModrinthServerError && err.v1Error) {
		notifications.addNotification({
			title: err.v1Error?.context ?? `An error occurred`,
			type: 'error',
			text: err.v1Error.description,
			errorCode: err.v1Error.error,
		})
	} else {
		notifications.addNotification({
			title: 'An error occurred',
			type: 'error',
			text: err.message ?? (err.data ? err.data.description : err),
		})
	}
}

export class ModrinthServer {
	readonly serverId: string
	private errors: Partial<Record<ModuleName, ModuleError>> = {}

	readonly general: GeneralModule
	readonly content: ContentModule
	readonly network: NetworkModule
	readonly startup: StartupModule

	constructor(serverId: string) {
		this.serverId = serverId

		this.general = new GeneralModule(this)
		this.content = new ContentModule(this)
		this.network = new NetworkModule(this)
		this.startup = new StartupModule(this)
	}

	async fetchConfigFile(fileName: string): Promise<any> {
		return await useServersFetch(`servers/${this.serverId}/config/${fileName}`)
	}

	constructServerProperties(properties: any): string {
		let fileContent = `#Minecraft server properties\n#${new Date().toUTCString()}\n`

		for (const [key, value] of Object.entries(properties)) {
			if (typeof value === 'object') {
				fileContent += `${key}=${JSON.stringify(value)}\n`
			} else if (typeof value === 'boolean') {
				fileContent += `${key}=${value ? 'true' : 'false'}\n`
			} else {
				fileContent += `${key}=${value}\n`
			}
		}

		return fileContent
	}

	async processImage(iconUrl: string | undefined): Promise<string | undefined> {
		const sharedImage = useState<string | undefined>(`server-icon-${this.serverId}`)

		if (sharedImage.value) {
			return sharedImage.value
		}

		try {
			const auth = await useServersFetch<JWTAuth>(`servers/${this.serverId}/fs`)
			try {
				const fileData = await useServersFetch(`/download?path=/server-icon-original.png`, {
					override: auth,
					retry: 1, // Reduce retries for optional resources
				})

				if (fileData instanceof Blob && import.meta.client) {
					const dataURL = await new Promise<string>((resolve) => {
						const canvas = document.createElement('canvas')
						const ctx = canvas.getContext('2d')
						const img = new Image()
						img.onload = () => {
							canvas.width = 512
							canvas.height = 512
							ctx?.drawImage(img, 0, 0, 512, 512)
							const dataURL = canvas.toDataURL('image/png')
							sharedImage.value = dataURL
							resolve(dataURL)
							URL.revokeObjectURL(img.src)
						}
						img.src = URL.createObjectURL(fileData)
					})
					return dataURL
				}
			} catch (error) {
				if (error instanceof ModrinthServerError) {
					if (error.statusCode && error.statusCode >= 500) {
						console.debug('Service unavailable, skipping icon processing')
						sharedImage.value = undefined
						return undefined
					}

					if (error.statusCode === 404 && iconUrl) {
						try {
							const response = await fetch(iconUrl)
							if (!response.ok) throw new Error('Failed to fetch icon')
							const file = await response.blob()
							const originalFile = new File([file], 'server-icon-original.png', {
								type: 'image/png',
							})

							if (import.meta.client) {
								const dataURL = await new Promise<string>((resolve) => {
									const canvas = document.createElement('canvas')
									const ctx = canvas.getContext('2d')
									const img = new Image()
									img.onload = () => {
										canvas.width = 64
										canvas.height = 64
										ctx?.drawImage(img, 0, 0, 64, 64)
										canvas.toBlob(async (blob) => {
											if (blob) {
												const scaledFile = new File([blob], 'server-icon.png', {
													type: 'image/png',
												})
												await useServersFetch(`/create?path=/server-icon.png&type=file`, {
													method: 'POST',
													contentType: 'application/octet-stream',
													body: scaledFile,
													override: auth,
												})
												await useServersFetch(`/create?path=/server-icon-original.png&type=file`, {
													method: 'POST',
													contentType: 'application/octet-stream',
													body: originalFile,
													override: auth,
												})
											}
										}, 'image/png')
										const dataURL = canvas.toDataURL('image/png')
										sharedImage.value = dataURL
										resolve(dataURL)
										URL.revokeObjectURL(img.src)
									}
									img.src = URL.createObjectURL(file)
								})
								return dataURL
							}
						} catch (externalError: any) {
							console.debug('Could not process external icon:', externalError.message)
						}
					}
				} else {
					throw error
				}
			}
		} catch (error: any) {
			console.debug('Icon processing failed:', error.message)
		}

		sharedImage.value = undefined
		return undefined
	}

	async testNodeReachability(): Promise<boolean> {
		if (!this.general?.node?.instance) {
			console.warn('No node instance available for ping test')
			return false
		}

		const wsUrl = `wss://${this.general.node.instance}/pingtest`

		try {
			return await new Promise((resolve) => {
				const socket = new WebSocket(wsUrl)
				const timeout = setTimeout(() => {
					socket.close()
					resolve(false)
				}, 5000)

				socket.onopen = () => {
					clearTimeout(timeout)
					socket.send(performance.now().toString())
				}

				socket.onmessage = () => {
					clearTimeout(timeout)
					socket.close()
					resolve(true)
				}

				socket.onerror = () => {
					clearTimeout(timeout)
					resolve(false)
				}
			})
		} catch (error) {
			console.error(`Failed to ping node ${wsUrl}:`, error)
			return false
		}
	}

	async refresh(
		modules: ModuleName[] = [],
		options?: {
			preserveConnection?: boolean
			preserveInstallState?: boolean
		},
	): Promise<void> {
		const modulesToRefresh =
			modules.length > 0 ? modules : (['general', 'content', 'network', 'startup'] as ModuleName[])

		for (const module of modulesToRefresh) {
			this.errors[module] = undefined

			try {
				switch (module) {
					case 'general': {
						if (options?.preserveConnection) {
							const currentImage = this.general.image
							const currentMotd = this.general.motd
							const currentStatus = this.general.status

							await this.general.fetch()

							if (currentImage) {
								this.general.image = currentImage
							}
							if (currentMotd) {
								this.general.motd = currentMotd
							}
							if (options.preserveInstallState && currentStatus === 'installing') {
								this.general.status = 'installing'
							}
						} else {
							await this.general.fetch()
						}
						break
					}
					case 'content':
						await this.content.fetch()
						break
					case 'network':
						await this.network.fetch()
						break
					case 'startup':
						await this.startup.fetch()
						break
				}
			} catch (error) {
				if (error instanceof ModrinthServerError) {
					if (error.statusCode === 404 && module === 'content') {
						console.debug(`Optional ${module} resource not found:`, error.message)
						continue
					}

					if (error.statusCode && error.statusCode >= 500) {
						console.debug(`Temporary ${module} unavailable:`, error.message)
						continue
					}
				}

				this.errors[module] = {
					error:
						error instanceof ModrinthServerError
							? error
							: new ModrinthServerError('Unknown error', undefined, error as Error),
					timestamp: Date.now(),
				}
			}
		}
	}

	get moduleErrors() {
		return this.errors
	}
}

export const useModrinthServers = async (
	serverId: string,
	includedModules: ModuleName[] = ['general'],
) => {
	const server = new ModrinthServer(serverId)
	await server.refresh(includedModules)
	return reactive(server)
}
