import { readFile } from "node:fs/promises";
import { MockAgent, setGlobalDispatcher } from "undici";
import type { IncomingHttpHeaders } from "undici/types/header.js";
import type {
	Interceptable,
	MockInterceptor,
} from "undici/types/mock-interceptor.js";
import { getVersion } from "../src/lokalise/pkg.js";
import type { HttpMethod } from "../src/types/http_method.js";

const packageVersion = await getVersion();

const mockAgent = new MockAgent();
setGlobalDispatcher(mockAgent);
mockAgent.disableNetConnect();

type StubParams = Partial<{
	uri: string;
	fixture: string;
	method: HttpMethod;
	query: Record<string, unknown>;
	body: Record<string, unknown>;
	reqHeaders: Record<string, string>;
	respHeaders: IncomingHttpHeaders;
	status: number;
	doFail: boolean;
	rootUrl: string;
	skipApiToken: boolean;
	version: string;
	data: string;
	delay: number;
}>;

export class Stub {
	private readonly uriPath: string;
	private readonly fixturePath: string;
	private readonly httpMethod: HttpMethod;
	private readonly requestHeaders: Record<string, string> | undefined;
	private readonly requestBody: string | undefined;
	private readonly responseHeaders: IncomingHttpHeaders | undefined;
	private readonly statusCode: number;
	private readonly doFail: boolean;
	private readonly rootUrl: string;
	private readonly version: string;
	private readonly data: string | undefined;
	private readonly delay: number;

	constructor(params: StubParams) {
		if (!params.uri) {
			throw new Error("The 'uri' parameter is required.");
		}

		const {
			uri,
			fixture,
			method = "GET",
			query,
			body,
			reqHeaders,
			respHeaders,
			status = 200,
			doFail = false,
			rootUrl = "https://api.lokalise.com",
			skipApiToken = false,
			version = "api2",
			data,
			delay,
		} = params;

		this.uriPath = this.buildUriPath(uri, query);
		this.fixturePath = fixture ? `./fixtures/${fixture}` : "";
		this.httpMethod = method;
		this.requestHeaders = this.buildRequestHeaders(
			skipApiToken,
			body,
			reqHeaders,
		);
		this.responseHeaders = respHeaders;
		this.requestBody = body ? JSON.stringify(body) : undefined;
		this.statusCode = status;
		this.doFail = doFail;
		this.rootUrl = rootUrl;
		this.version = version;
		this.data = data;
		this.delay = delay ?? 0;
	}

	async setStub() {
		const mockPool = mockAgent.get(this.rootUrl);

		try {
			await this.setupMock(mockPool);
		} catch (error) {
			throw new Error(
				`Failed to set up mock for URI: ${this.uriPath} - ${error.message}`,
			);
		}
	}

	private async setupMock(mockPool: Interceptable) {
		const mockOpts = this.buildMockOptions();
		const respOpts = this.buildResponseOptions();

		await this.respond(mockPool, mockOpts, this.doFail, respOpts);
	}

	private async respond(
		mockPool: Interceptable,
		mockOpts: MockInterceptor.Options,
		isError: boolean,
		respOpts: MockInterceptor.MockResponseOptions,
	) {
		if (isError) {
			mockPool.intercept(mockOpts).replyWithError(new Error("Fail"));
		} else {
			const responseData = this.data ?? (await this.readFixture());
			const pool = mockPool
				.intercept({ ...mockOpts, body: this.requestBody })
				.reply(this.statusCode, responseData, respOpts);

			if (this.delay > 0) {
				pool.delay(this.delay);
			}
		}
	}

	private async readFixture(): Promise<Record<string, unknown> | string> {
		if (!this.fixturePath) {
			return "";
		}

		try {
			const fileContent = await readFile(
				new URL(this.fixturePath, import.meta.url),
			);
			return JSON.parse(fileContent.toString());
		} catch (error) {
			console.error(
				`Error reading fixture file at ${this.fixturePath}:`,
				error,
			);
			throw error;
		}
	}

	private buildRequestHeaders(
		skipApiToken: boolean,
		body: Record<string, unknown> | undefined,
		reqHeaders?: Record<string, string>,
	): Record<string, string> {
		return {
			Accept: "application/json",
			"User-Agent": `node-lokalise-api/${packageVersion}`,
			...(skipApiToken ? {} : { "x-api-token": String(process.env.API_KEY) }),
			...(body ? { "Content-type": "application/json" } : {}),
			...reqHeaders,
		};
	}

	private buildUriPath(uri: string, query?: Record<string, unknown>): string {
		if (!query) {
			return uri;
		}

		const stringifiedQuery = Object.fromEntries(
			Object.entries(query)
				.filter(([, value]) => value !== undefined && value !== null)
				.map(([key, value]) => [key, String(value)]),
		);

		return `${uri}?${new URLSearchParams(stringifiedQuery).toString()}`;
	}

	private buildMockOptions(): MockInterceptor.Options {
		return {
			method: this.httpMethod,
			path: `/${this.version}/${this.uriPath}`,
			headers: this.requestHeaders,
		};
	}

	private buildResponseOptions(): MockInterceptor.MockResponseOptions {
		return {
			headers: this.responseHeaders,
		};
	}
}
