import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import Soup from 'gi://Soup';

import { UserAgent, getCachePath } from '../common/constants.js';

var OutputStreamSpliceFlags = Gio.OutputStreamSpliceFlags;
Gio._promisify(Soup.Session.prototype, 'send_async');
Gio._promisify(Gio.File.prototype, 'replace_async');
Gio._promisify(Gio.File.prototype, 'replace_contents_async');
Gio._promisify(Gio.OutputStream.prototype, 'splice_async');

export async function tryGetMetadata(ext, url, cancellable) {
	const empty = { title: null, description: null, image: null };
	let session = null;
	try {
		// Check if URL is valid
		const uri = GLib.uri_parse(url, GLib.UriFlags.NONE);

		// Create request
		session = new Soup.Session({ user_agent: UserAgent, idle_timeout: 5 });
		const message = Soup.Message.new_from_uri('GET', uri);

		// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
		message.request_headers.append('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8');

		// Send request
		const response = await session.send_async(message, GLib.PRIORITY_DEFAULT, cancellable);
		if (response == null) return empty;

		// Check if the response is an image
		const [contentType] = message.response_headers.get_content_type();
		if (contentType && Gio.content_type_is_a(contentType, 'image/*')) {
			// Since the response has already been received, write image to cache
			const imagePath = getLinkImagePath(ext, url);
			if (imagePath == null) return empty;

			// Write to cache
			if (!imagePath.query_exists(cancellable)) {
				const out = await imagePath.replace_async(
					null,
					false,
					Gio.FileCreateFlags.REPLACE_DESTINATION,
					GLib.PRIORITY_DEFAULT,
					null,
				);
				await out.splice_async(
					response,
					Gio.OutputStreamSpliceFlags.CLOSE_SOURCE | Gio.OutputStreamSpliceFlags.CLOSE_TARGET,
					GLib.PRIORITY_DEFAULT,
					null,
				);
			}
			return { title: null, description: null, image: url };
		}

		// Download page
		if (!contentType || !Gio.content_type_is_a(contentType, 'text/html')) return empty;
		const out = Gio.MemoryOutputStream.new_resizable();
		await out.splice_async(
			response,
			OutputStreamSpliceFlags.CLOSE_SOURCE | Gio.OutputStreamSpliceFlags.CLOSE_TARGET,
			GLib.PRIORITY_DEFAULT,
			cancellable,
		);
		const bytes = out.steal_as_bytes();
		const data = bytes.get_data();
		if (data == null) return empty;

		// Extract metadata
		const text = new TextDecoder().decode(data);

		// This regex is enough for most cases without having to resort to using a HTML parser
		const metaRegex = /<meta\s+(?:property|name)="([^"]*?)"\s+content="([^"]*?)"/gm;
		const reverseMetaRegex = /<meta\s+content="([^"]*?)"\s+(?:property|name)="([^"]*?)"/gm;
		const meta = {};
		for (const match of text.matchAll(metaRegex)) meta[match[1]] = match[2];
		for (const match of text.matchAll(reverseMetaRegex)) meta[match[2]] = match[1];

		// Title
		let title = meta['title'] ?? meta['og:title'] ?? meta['twitter:title'] ?? null;
		if (title == null) {
			const titleRegex = /<title>\s*(.*?)\s*<\/title>/s;
			const titleMatch = text.match(titleRegex);
			title = titleMatch ? titleMatch[1] : null;
		}

		// Description
		let description = meta['description'] ?? meta['og:description'] ?? meta['twitter:description'] ?? null;

		// Image
		let image =
			meta['image'] ??
			meta['og:image'] ??
			meta['og:image:url'] ??
			meta['og:image:secure_url'] ??
			meta['twitter:image'] ??
			null;
		if (image) {
			try {
				// Resolve relative image paths
				image = uri.parse_relative(image, GLib.UriFlags.NONE).to_string();
			} catch {
				image = null;
			}
		}

		// Escape
		title = title ? decodeHtml(title).trim() : null;
		description = description ? decodeHtml(description).trim() : null;
		return { title: title, description: description, image: image };
	} catch (err) {
		ext.logger.error('Failed to get metadata', err);
	} finally {
		session?.abort();
	}
	return empty;
}

function decodeHtml(html) {
	return html
		.replace(/&amp;/g, '&') // Pre-check for `&amp;` to fix wrong encoding on many websites
		.replace(/&([^;]+);/g, (_, s) => {
			switch (s) {
				case 'lt':
					return '<';
				case 'gt':
					return '>';
				case 'amp':
					return '&';
				case 'quot':
					return '"';
				case 'apos':
					return "'";
				case 'mdash':
					return '—';
				case 'ndash':
					return '–';
				default:
					return '';
			}
		})
		.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(+n));
}

export function getLinkImagePath(ext, url) {
	const checksum = GLib.compute_checksum_for_string(GLib.ChecksumType.MD5, url, url.length);
	if (checksum == null) return null;
	const cacheDir = getCachePath(ext);
	if (!cacheDir.query_exists(null)) cacheDir.make_directory_with_parents(null);
	return cacheDir.get_child(checksum);
}

export async function tryGetLinkImage(ext, url, cancellable) {
	let session = null;
	try {
		// Check if URL is valid
		const uri = GLib.uri_parse(url, GLib.UriFlags.NONE);

		// Check if image is already cached
		const imagePath = getLinkImagePath(ext, url);
		if (imagePath == null) return null;
		if (imagePath.query_exists(cancellable)) return imagePath;

		// Otherwise download image
		session = new Soup.Session({ user_agent: UserAgent, idle_timeout: 5 });
		const message = Soup.Message.new_from_uri('GET', uri);

		// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
		message.request_headers.append(
			'Accept',
			'image/avif,image/webp,image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5',
		);

		// Send request
		const response = await session.send_and_read_async(message, GLib.PRIORITY_DEFAULT, cancellable);
		if (response == null) return null;
		const data = response.get_data();
		if (data == null) return null;

		// Check if the response is an image
		const [contentType] = message.response_headers.get_content_type();
		if (contentType == null || !contentType.startsWith('image/')) return null;

		// Write to cache
		await imagePath.replace_contents_async(data, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, cancellable);
		return imagePath;
	} catch {
		ext.logger.error('Failed to get link image');
	} finally {
		session?.abort();
	}
	return null;
}
