import { getSetCookies } from '@std/http/cookie';
import { generateTotp } from './totp.ts';

export interface API_CLIENT {
	fetch: (url: string, options?: FETCH_OPTIONS, transform?: (obj: any) => any) => Promise<any>;
}

export type API_CONFIG = {
	protocol: string;
	hostname: string;
	port: number;
	prefix: string;
};

const DEFAULT_API_CONFIG: API_CONFIG = {
	protocol: 'http:',
	hostname: 'localhost',
	port: 80,
	prefix: ''
};

export interface RETRY_OPTIONS {
	limit: number;
	methods: string[];
	status_codes: number[];
}

export interface FETCH_OPTIONS extends RequestInit {
	retry?: RETRY_OPTIONS;
	json?: Record<string, any>;
	done?: (response: Response) => void;
	session?: Record<string, any>;
	totp_token?: string;
}

const DEFAULT_TRANSFORM = (response_json: any) => {
	return response_json;
};

export function api(api_config?: Record<string, any>): API_CLIENT {
	const config: API_CONFIG = {
		...DEFAULT_API_CONFIG,
		...(api_config ?? {})
	};

	const client: API_CLIENT = {
		fetch: async (url: string, options?: FETCH_OPTIONS, transform?: (obj: any) => any): Promise<any> => {
			const prefix: string = `${config.protocol}//${config.hostname}:${config.port}${config.prefix}`;
			const retry: RETRY_OPTIONS = options?.retry ?? {
				limit: 0,
				methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'],
				status_codes: [500, 502, 503, 504, 521, 522, 524]
			};

			const content_type = options?.json ? 'application/json' : 'text/plain';
			const headers = new Headers(options?.headers ?? {});
			headers.append('accept', 'application/json');
			if (options?.json || options?.body) {
				headers.append('content-type', content_type);
			}
			if (options?.session) {
				const cookies = getSetCookies(headers);

				cookies.push({
					name: options.totp_token ?? 'totp',
					value: await generateTotp(options.session.secret),
					maxAge: 29,
					expires: Date.now() + 29_000,
					path: '/'
				});

				for (const cookie of cookies) {
					headers.append(`x-${cookie.name}`, cookie.value);
				}
				headers.append('cookie', cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; '));
			}

			const request_options: RequestInit = {
				body: options?.json ? JSON.stringify(options.json, null, 2) : options?.body,
				method: options?.method ?? 'GET',
				credentials: options?.credentials ?? 'include',
				redirect: options?.redirect ?? 'follow',
				headers,
				signal: options?.signal
			};

			const response_transform = transform ?? DEFAULT_TRANSFORM;

			let retries = 0;
			let delay = 1000;
			const resolved_url = `${prefix}${url}`;
			do {
				const response: Response = await fetch(resolved_url, request_options);
				if (
					retries < retry.limit && retry.status_codes.includes(response.status) &&
					retry.methods.includes(request_options.method ?? 'GET')
				) {
					++retries;
					await new Promise((resolve) => setTimeout(resolve, delay));
					delay *= 2;
					continue;
				}

				if (response.status >= 400) {
					const error_response = await response.json();
					throw new Error(
						error_response.error?.message ?? 'Bad Reqeest',
						error_response.error ?? {
							cause: error_response?.cause ?? JSON.stringify(error_response)
						}
					);
				}

				const data = await response.json();
				const transformed = await response_transform(data);

				if (options?.done) {
					options.done(response);
				}

				return transformed;
			} while (retries < retry.limit);
		}
	};

	return client;
}
