import 'isomorphic-unfetch'

export interface File {
	url: string
	shortName: string
	originalName: string
	mimeType: string
	uncachedHits: number
	updatedAt: number
	size: number
	parent?: string
	private: boolean
	ephemeralTimestamp: number | null
	hasPassword: boolean
}

export interface Folder {
	id: string
	name: string
	parent?: string
}

export type User = {
	id: string
	name: string
	email?: string
	admin: boolean
	usage: number
	preferredDomain: string
}

export type UsageInfo = {
	username: string
	usage: number
	email: string
}

export class PatClient {
	private baseUrl: string
	token?: string

	constructor(baseUrl?: string) {
		this.baseUrl = baseUrl ?? 'https://pat.doggo.ninja'
	}

	async files(parent: string | undefined): Promise<File[]> {
		return await this.makeRequest('get', '/v1/files', { parent })
	}

	async upload(
		file: globalThis.File,
		parent: string | undefined,
		onProgress?: (loaded: number, total: number) => void
	): Promise<File> {
		const url = new URL(`${this.baseUrl}/v1/upload`)
		if (parent) url.searchParams.set('parent', parent)
		url.searchParams.set('mimeType', file.type)
		if (file.name) url.searchParams.set('originalName', file.name)

		return await this.makeUploadRequest(
			'post',
			url.toString(),
			file,
			onProgress
		)
	}

	async replace(
		shortName: string,
		file: globalThis.File,
		onProgress?: (loaded: number, total: number) => void
	): Promise<File> {
		const url = new URL(
			`${this.baseUrl}/v1/file/${encodeURIComponent(shortName)}`
		)
		url.searchParams.set('mimeType', file.type)
		if (file.name) url.searchParams.set('originalName', file.name)

		return await this.makeUploadRequest('put', url.toString(), file, onProgress)
	}

	async getDownloadToken(
		shortName: string,
		password?: string
	): Promise<string> {
		const { downloadToken } = await this.makeRequest<{ downloadToken: string }>(
			'post',
			'/v1/files/token',
			{},
			{ shortName, password }
		)
		return downloadToken
	}

	async updateFileSharing(
		shortName: string,
		isPrivate: boolean,
		details: { ephemeralTimestamp: number | null; password: string | boolean }
	): Promise<File> {
		return await this.makeRequest(
			'post',
			'/v1/files/sharing',
			{},
			{ shortName, private: isPrivate, ...details }
		)
	}

	async moveFile(
		shortName: string,
		details: { originalName?: string; parent?: string | undefined },
		copy?: boolean
	): Promise<File> {
		return await this.makeRequest(
			'post',
			'/v1/files/move',
			{},
			{
				shortName,
				...details,
				forceMove: 'parent' in details,
				copy
			}
		)
	}

	async getFile(shortName: string): Promise<File> {
		return await this.makeRequest(
			'get',
			`/v1/file/${encodeURIComponent(shortName)}`
		)
	}

	async deleteFile(shortName: string): Promise<File> {
		return await this.makeRequest(
			'delete',
			`/v1/file/${encodeURIComponent(shortName)}`
		)
	}

	async deleteFolder(id: string): Promise<File> {
		return await this.makeRequest(
			'delete',
			`/v1/folder/${encodeURIComponent(id)}`
		)
	}

	async folders(parent: string | undefined): Promise<Folder[]> {
		return await this.makeRequest('get', '/v1/folders', { parent })
	}

	async createFolder(
		name: string,
		parent: string | undefined
	): Promise<Folder> {
		return await this.makeRequest(
			'post',
			'/v1/folders/create',
			{},
			{ name, parent }
		)
	}

	async moveFolder(
		id: string,
		details: { name?: string; parent?: string | undefined }
	): Promise<File> {
		return await this.makeRequest(
			'post',
			'/v1/folders/move',
			{},
			{
				id,
				...details,
				forceMove: 'parent' in details
			}
		)
	}

	async getFolder(id: string): Promise<Folder[]> {
		return await this.makeRequest('get', `/v1/folder/${encodeURIComponent(id)}`)
	}

	async checkAuth(): Promise<boolean> {
		try {
			await this.makeRequest('get', '/v1/auth/check')
			return true
		} catch {
			return false
		}
	}

	async login(
		username: string,
		password: string
	): Promise<{ sessionToken: string; expiration: Date }> {
		const response = await this.makeRequest<{
			sessionToken: string
			expiration: number
		}>(
			'post',
			'/v1/auth/login',
			{},
			{
				name: username,
				password
			}
		)
		return {
			sessionToken: response.sessionToken,
			expiration: new Date(response.expiration)
		}
	}

	async resetPassword(nameOrEmail: string): Promise<void> {
		await this.makeRequest('post', '/v1/auth/reset', {}, { nameOrEmail })
	}

	async completeResetPassword(
		resetToken: string,
		newPassword: string,
		regenerateAccessToken: boolean
	): Promise<void> {
		await this.makeRequest(
			'post',
			'/v1/auth/reset/complete',
			{},
			{
				resetToken,
				newPassword,
				regenerateAccessToken
			}
		)
	}

	async invalidateSession(): Promise<void> {
		await this.makeRequest('get', '/v1/auth/invalidate')
	}

	async regenerateAccessToken(): Promise<string> {
		const { newToken } = await this.makeRequest<{ newToken: string }>(
			'get',
			'/v1/auth/regenerate'
		)
		return newToken
	}

	async me(): Promise<User> {
		return await this.makeRequest('get', '/v1/me')
	}

	async setDomain(domain: 'doggo.ninja' | 'ninja.dog'): Promise<User> {
		return await this.makeRequest('post', '/v1/domain', {}, { domain })
	}

	async adminUsage(): Promise<UsageInfo[]> {
		return await this.makeRequest('get', '/v1/admin/usage')
	}

	async makeUser(username: string, email: string): Promise<void> {
		await this.makeRequest(
			'post',
			'/v1/admin/mkuser',
			{},
			{
				name: username,
				email
			}
		)
	}

	async makeUploadRequest<Type = {}>(
		method: 'post' | 'put' | 'delete',
		url: string,
		file: globalThis.File,
		onProgress?: (loaded: number, total: number) => void
	): Promise<Type> {
		if (typeof 'window' === 'undefined') {
			throw new Error('File uploads only supported in browser')
		}

		return new Promise((resolve, reject) => {
			const xhr = new XMLHttpRequest()

			xhr.upload.addEventListener('progress', event => {
				onProgress && onProgress(event.loaded, event.total)
			})

			xhr.addEventListener('readystatechange', () => {
				if (xhr.readyState === 4) {
					const body = xhr.responseText
					try {
						const json = JSON.parse(body)
						if (xhr.status >= 200 && xhr.status < 300) {
							resolve(json)
						} else {
							reject(
								new Error(json.message || xhr.statusText || 'No error info')
							)
						}
					} catch {
						reject(new Error('Unable to parse json'))
					}
				}
			})

			xhr.addEventListener('error', () => {
				reject(new Error('An unexpected error occurred'))
			})

			xhr.open(method, url)
			xhr.setRequestHeader('Authorization', `Bearer ${this.token}`)
			xhr.setRequestHeader('Content-Type', 'application/octet-stream')
			xhr.send(file)
		})
	}

	async makeRequest<Type = {}>(
		method: 'get' | 'post' | 'put' | 'delete',
		path: string,
		query: Record<string, string | undefined> = {},
		body?: Record<string, unknown>
	): Promise<Type> {
		const headers: HeadersInit = {
			Accept: 'application/json'
		}
		if (this.token) headers['Authorization'] = `Bearer ${this.token}`
		if (body) headers['Content-Type'] = 'application/json'

		const url = new URL(this.baseUrl.concat(path))
		for (const [key, value] of Object.entries(query)) {
			if (value) url.searchParams.set(key, value)
		}

		const res = await fetch(url.toString(), {
			method,
			headers,
			body: JSON.stringify(body)
		})

		if (!res.ok) {
			let message
			try {
				message = (await res.json()).message
			} catch {
				message = res.statusText
			}
			message = message ?? 'No error info'
			throw new Error(message)
		}

		const json = await res.json()
		return json as Type
	}
}
