import DOMException from '../exception/DOMException.js';
import * as PropertySymbol from '../PropertySymbol.js';
import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js';
import type { THeadersInit } from './types/THeadersInit.js';
import type BrowserWindow from '../window/BrowserWindow.js';

/**
 * Fetch headers.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Headers
 */
export default class Headers {
	// Injected by WindowContextClassExtender
	protected declare [PropertySymbol.window]: BrowserWindow;

	public [PropertySymbol.entries]: { [k: string]: { name: string; value: string[] } } = {};

	/**
	 * Constructor.
	 *
	 * @param init Headers init.
	 */
	constructor(init?: THeadersInit | null) {
		if (init) {
			if (init instanceof Headers) {
				this[PropertySymbol.entries] = JSON.parse(JSON.stringify(init[PropertySymbol.entries]));
			} else if (Array.isArray(init)) {
				for (const entry of init) {
					if (entry.length !== 2) {
						throw new DOMException(
							'Failed to construct "Headers": The provided init is not a valid array.',
							DOMExceptionNameEnum.invalidStateError
						);
					}
					this.append(entry[0], entry[1]);
				}
			} else {
				for (const name of Object.keys(init)) {
					this.set(name, init[name]);
				}
			}
		}
	}

	/**
	 * Appends a new value onto an existing header inside a Headers object, or adds the header if it does not already exist.
	 *
	 * @param name Name.
	 * @param value Value.
	 */
	public append(name: string, value: string): void {
		const lowerName = name.toLowerCase();
		if (this[PropertySymbol.entries][lowerName]) {
			this[PropertySymbol.entries][lowerName].value.push(value);
		} else {
			this[PropertySymbol.entries][lowerName] = {
				name,
				value: [value]
			};
		}
	}

	/**
	 * Removes an header.
	 *
	 * @param name Name.
	 */
	public delete(name: string): void {
		delete this[PropertySymbol.entries][name.toLowerCase()];
	}

	/**
	 * Returns header value.
	 *
	 * @param name Name.
	 * @returns Value.
	 */
	public get(name: string): string | null {
		return this[PropertySymbol.entries][name.toLowerCase()]?.value.join(', ') ?? null;
	}

	/**
	 * Sets a new value for an existing header inside a Headers object, or adds the header if it does not already exist.
	 *
	 * @param name Name.
	 * @param value Value.
	 */
	public set(name: string, value: string): void {
		this[PropertySymbol.entries][name.toLowerCase()] = {
			name,
			value: [value]
		};
	}

	/**
	 * Returns an array containing the values of all Set-Cookie headers associated with a response.
	 *
	 * @returns An array of strings representing the values of all the different Set-Cookie headers.
	 */
	public getSetCookie(): string[] {
		const entry = this[PropertySymbol.entries]['set-cookie'];
		if (!entry) {
			return [];
		}
		return entry.value;
	}

	/**
	 * Returns whether an Headers object contains a certain key.
	 *
	 * @param name Name.
	 * @returns "true" if the Headers object contains the key.
	 */
	public has(name: string): boolean {
		return !!this[PropertySymbol.entries][name.toLowerCase()];
	}

	/**
	 * Executes a callback function once per each key/value pair in the Headers object.
	 *
	 * @param callback Callback.
	 * @param thisArg thisArg.
	 */
	public forEach(
		callback: (value: string, name: string, parent: this) => void,
		thisArg?: any
	): void {
		const thisArgValue = thisArg ?? this[PropertySymbol.window];
		for (const header of Object.values(this[PropertySymbol.entries])) {
			callback.call(thisArgValue, header.value.join(', '), header.name, this);
		}
	}

	/**
	 * Returns an iterator, allowing you to go through all keys of the key/value pairs contained in this object.
	 *
	 * @returns Iterator.
	 */
	public *keys(): ArrayIterator<string> {
		for (const header of Object.values(this[PropertySymbol.entries])) {
			yield header.name;
		}
	}

	/**
	 * Returns an iterator, allowing you to go through all values of the key/value pairs contained in this object.
	 *
	 * @returns Iterator.
	 */
	public *values(): ArrayIterator<string> {
		for (const header of Object.values(this[PropertySymbol.entries])) {
			yield header.value.join(', ');
		}
	}

	/**
	 * Returns an iterator, allowing you to go through all key/value pairs contained in this object.
	 *
	 * @returns Iterator.
	 */
	public *entries(): ArrayIterator<[string, string]> {
		for (const header of Object.values(this[PropertySymbol.entries])) {
			yield [header.name, header.value.join(', ')];
		}
	}

	/**
	 * Iterator.
	 *
	 * @returns Iterator.
	 */
	public *[Symbol.iterator](): ArrayIterator<[string, string]> {
		for (const header of Object.values(this[PropertySymbol.entries])) {
			yield [header.name, header.value.join(', ')];
		}
	}
}
