import type BrowserWindow from '../window/BrowserWindow.js';
import URL from '../url/URL.js';
import Fetch from './Fetch.js';
import SyncFetch from './SyncFetch.js';
import type { TRequestCredentials } from './types/TRequestCredentials.js';
import WindowBrowserContext from '../window/WindowBrowserContext.js';
import PreloadUtility from './preload/PreloadUtility.js';
import * as PropertySymbol from '../PropertySymbol.js';
import type { TRequestReferrerPolicy } from './types/TRequestReferrerPolicy.js';
import type IResourceFetchResponse from './types/IResourceFetchResponse.js';

/**
 * Helper class for performing fetch of resources.
 */
export default class ResourceFetch {
	private window: BrowserWindow;

	/**
	 * Constructor.
	 *
	 * @param window Window.
	 */
	constructor(window: BrowserWindow) {
		this.window = window;
	}

	/**
	 * Returns resource data asynchronously.
	 *
	 * @param url URL.
	 * @param destination Destination.
	 * @param [options]
	 * @param [options.credentials] Credentials.
	 * @param options.referrerPolicy
	 * @returns Response.
	 */
	public async fetch(
		url: string | URL,
		destination: 'script' | 'style' | 'module',
		options?: { credentials?: TRequestCredentials; referrerPolicy?: TRequestReferrerPolicy }
	): Promise<IResourceFetchResponse> {
		const browserFrame = new WindowBrowserContext(this.window).getBrowserFrame();

		if (!browserFrame) {
			return {
				content: '',
				virtualServerFile: null
			};
		}

		// Preloaded resource
		if (destination === 'script' || destination === 'style') {
			const preloadKey = PreloadUtility.getKey({
				url: String(url),
				destination,
				mode: 'cors',
				credentialsMode: options?.credentials || 'same-origin'
			});
			const preloadEntry = this.window.document[PropertySymbol.preloads].get(preloadKey);

			if (preloadEntry) {
				this.window.document[PropertySymbol.preloads].delete(preloadKey);

				const response = preloadEntry.response || (await preloadEntry.onResponseAvailable());

				if (response && !response.ok) {
					throw new this.window.DOMException(
						`Failed to perform request to "${
							new URL(url, this.window.location.href).href
						}". Status ${preloadEntry.response?.status || '0'} ${preloadEntry.response?.statusText || 'Unknown'}.`
					);
				}

				return {
					content: preloadEntry.response?.[PropertySymbol.buffer]?.toString() || '',
					virtualServerFile: preloadEntry.response?.[PropertySymbol.virtualServerFile] || null
				};
			}
		}

		const fetch = new Fetch({
			browserFrame,
			window: this.window,
			url,
			disableSameOriginPolicy: destination === 'script' || destination === 'style',
			disablePreload: true,
			init: {
				credentials: options?.credentials,
				referrerPolicy: options?.referrerPolicy
			}
		});

		const response = await fetch.send();

		if (!response.ok) {
			throw new this.window.DOMException(
				`Failed to perform request to "${new URL(url, this.window.location.href).href}". Status ${
					response.status
				} ${response.statusText}.`
			);
		}

		return {
			content: await response.text(),
			virtualServerFile: response[PropertySymbol.virtualServerFile] || null
		};
	}

	/**
	 * Returns resource data synchronously.
	 *
	 * @param url URL.
	 * @param destination Destination.
	 * @param [options] Options.
	 * @param [options.credentials] Credentials.
	 * @param [options.referrerPolicy] Referrer policy.
	 * @returns Response.
	 */
	public fetchSync(
		url: string,
		destination: 'script' | 'style' | 'module',
		options?: { credentials?: TRequestCredentials; referrerPolicy?: TRequestReferrerPolicy }
	): IResourceFetchResponse {
		const browserFrame = new WindowBrowserContext(this.window).getBrowserFrame();

		if (!browserFrame) {
			return {
				content: '',
				virtualServerFile: null
			};
		}

		// Preloaded resource
		if (destination === 'script' || destination === 'style') {
			const preloadKey = PreloadUtility.getKey({
				url: String(url),
				destination,
				mode: 'cors',
				credentialsMode: options?.credentials || 'same-origin'
			});
			const preloadEntry = this.window.document[PropertySymbol.preloads].get(preloadKey);

			// We will only use this if the fetch for the resource is complete as it is async and this request is sync.
			if (preloadEntry && preloadEntry.response) {
				this.window.document[PropertySymbol.preloads].delete(preloadKey);

				const response = preloadEntry.response;

				if (!response.ok) {
					throw new this.window.DOMException(
						`Failed to perform request to "${
							new URL(url, this.window.location.href).href
						}". Status ${preloadEntry.response.status} ${preloadEntry.response.statusText}.`
					);
				}

				return {
					content: preloadEntry.response?.[PropertySymbol.buffer]?.toString() || '',
					virtualServerFile: preloadEntry.response?.[PropertySymbol.virtualServerFile] || null
				};
			}
		}

		const fetch = new SyncFetch({
			browserFrame,
			window: this.window,
			url,
			disableSameOriginPolicy: true,
			init: {
				credentials: options?.credentials,
				referrerPolicy: options?.referrerPolicy
			}
		});

		const response = fetch.send();

		if (!response.ok) {
			throw new this.window.DOMException(
				`Failed to perform request to "${new URL(url, this.window.location.href).href}". Status ${
					response.status
				} ${response.statusText}.`
			);
		}

		return {
			content: response.body?.toString() || '',
			virtualServerFile: response[PropertySymbol.virtualServerFile] || null
		};
	}
}
