import { config } from "./config.util.js";
import {
	classifyApiError,
	createApiError,
	createAuthInvalidError,
	createNetworkError,
	createUnexpectedError,
	McpError,
} from "./error.util.js";
import { Logger } from "./logger.util.js";

// Create a contextualized logger for this file
const transportLogger = Logger.forContext("utils/transport.util.ts");

// Log transport utility initialization
transportLogger.debug("Transport utility initialized");

/**
 * Interface for IP API credentials.
 * Note: API token is optional for the free tier.
 */
export interface IpApiCredentials {
	apiToken?: string;
}

/**
 * Interface for Lokalise API credentials.
 * Note: API token is required for all operations.
 */
export interface LokaliseApiCredentials {
	apiToken: string;
}

/**
 * Interface for HTTP request options
 */
export interface RequestOptions {
	method?: "GET" | "POST" | "PUT" | "DELETE";
	headers?: Record<string, string>;
	body?: unknown;
}

/**
 * Retrieves IP API credentials from configuration.
 * Specifically checks for IPAPI_API_TOKEN.
 * @returns IpApiCredentials object containing the API token if found.
 */
export function getIpApiCredentials(): IpApiCredentials {
	const methodLogger = Logger.forContext(
		"utils/transport.util.ts",
		"getIpApiCredentials",
	);

	const apiToken = config.get("IPAPI_API_TOKEN");

	if (!apiToken) {
		methodLogger.debug(
			"No IP API token found (IPAPI_API_TOKEN). Using free tier.",
		);
		return {}; // Return empty object if no token
	}
	methodLogger.debug("Using IP API token from configuration.");
	return { apiToken };
}

/**
 * Retrieves Lokalise API credentials from configuration.
 * Specifically checks for LOKALISE_API_KEY.
 * @returns LokaliseApiCredentials object containing the API token.
 * @throws {McpError} If no API token is found.
 */
export function getLokaliseApiCredentials(): LokaliseApiCredentials {
	const methodLogger = Logger.forContext(
		"utils/transport.util.ts",
		"getLokaliseApiCredentials",
	);

	const apiToken = config.get("LOKALISE_API_KEY");

	if (!apiToken) {
		methodLogger.error("No Lokalise API token found (LOKALISE_API_KEY)");
		throw createAuthInvalidError(
			"Lokalise API token is required. Please set LOKALISE_API_KEY environment variable.",
		);
	}

	methodLogger.debug("Using Lokalise API token from configuration.");
	return { apiToken };
}

/**
 * Fetches data specifically from the ip-api.com endpoint.
 * Handles URL construction, authentication (if token provided), and query parameters.
 * Relies on the generic fetchApi function for the actual HTTP request.
 *
 * @param path The specific IP address or path component (e.g., "8.8.8.8"). Empty string for current IP.
 * @param options Additional options like HTTP method, headers, body, and ip-api specific params.
 * @param options.useHttps - Use HTTPS (requires paid plan for ip-api.com). Defaults to false.
 * @param options.fields - Specific fields to request from ip-api.com.
 * @param options.lang - Language code for response data.
 * @returns The response data parsed as type T.
 * @throws {McpError} If the request fails, including network errors, API errors, or parsing issues.
 */
export async function fetchIpApi<T>(
	path: string,
	options: RequestOptions & {
		useHttps?: boolean;
		fields?: string[];
		lang?: string;
	} = {},
): Promise<T> {
	const methodLogger = Logger.forContext(
		"utils/transport.util.ts",
		"fetchIpApi",
	);

	// Get credentials (token might be undefined)
	const credentials = getIpApiCredentials();

	// Determine protocol based on options
	const protocol = options.useHttps ? "https" : "http";
	const baseUrl = `${protocol}://ip-api.com/json`;

	// Format path for URL
	const normalizedPath = path ? `/${path}` : "";
	let url = `${baseUrl}${normalizedPath}`;

	// Build query parameters
	const queryParams = new URLSearchParams();

	// Add API token if present
	if (credentials.apiToken) {
		queryParams.set("key", credentials.apiToken);
		methodLogger.debug("API token added to query parameters.");
	}

	// Add fields parameter
	if (options.fields?.length) {
		queryParams.set("fields", options.fields.join(","));
		methodLogger.debug(`Requesting fields: ${options.fields.join(",")}`);
	}

	// Add language parameter
	if (options.lang) {
		queryParams.set("lang", options.lang);
		methodLogger.debug(`Requesting language: ${options.lang}`);
	}

	// Append query string if needed
	const queryString = queryParams.toString();
	if (queryString) {
		url += `?${queryString}`;
	}

	methodLogger.debug(`Constructed URL: ${url}`);

	// Delegate the actual fetch call to the generic fetchApi
	return fetchApi<T>(url, {
		method: options.method,
		headers: options.headers,
		body: options.body,
	});
}

/**
 * Fetches data specifically from the Lokalise API endpoint.
 * Handles URL construction, authentication, and query parameters.
 * Relies on the generic fetchApi function for the actual HTTP request.
 *
 * @param path The API path (e.g., "/projects", "/languages").
 * @param options Additional options like HTTP method, headers, and body.
 * @returns The response data parsed as type T.
 * @throws {McpError} If the request fails, including network errors, API errors, or parsing issues.
 */
export async function fetchLokaliseApi<T>(
	path: string,
	options: RequestOptions = {},
): Promise<T> {
	const methodLogger = Logger.forContext(
		"utils/transport.util.ts",
		"fetchLokaliseApi",
	);

	// Get credentials (token is required)
	const credentials = getLokaliseApiCredentials();

	// Construct the full URL
	const baseUrl = "https://api.stage.lokalise.cloud/api2//api2";
	const url = `${baseUrl}${path}`;

	methodLogger.debug(`Constructed Lokalise API URL: ${url}`);

	// Delegate the actual fetch call to the generic fetchApi with Lokalise headers
	return fetchApi<T>(url, {
		method: options.method,
		headers: {
			"X-Api-Token": credentials.apiToken,
			...options.headers,
		},
		body: options.body,
	});
}

/**
 * Generic and reusable function to fetch data from any API endpoint.
 * Handles standard HTTP request setup, response checking, basic error handling, and logging.
 *
 * @param url The full URL to fetch data from.
 * @param options Request options including method, headers, and body.
 * @returns The response data parsed as type T.
 * @throws {McpError} If the request fails, including network errors, non-OK HTTP status, or JSON parsing issues.
 */
export async function fetchApi<T>(
	url: string,
	options: RequestOptions = {},
): Promise<T> {
	const methodLogger = Logger.forContext("utils/transport.util.ts", "fetchApi");

	// Prepare standard request options
	const requestOptions: RequestInit = {
		method: options.method || "GET",
		headers: {
			// Standard headers, allow overrides via options.headers
			"Content-Type": "application/json",
			Accept: "application/json",
			...options.headers,
		},
		body: options.body ? JSON.stringify(options.body) : undefined,
	};

	methodLogger.debug(`Executing API call: ${requestOptions.method} ${url}`);
	const startTime = performance.now(); // Track performance

	try {
		const response = await fetch(url, requestOptions);
		const endTime = performance.now();
		const duration = (endTime - startTime).toFixed(2);

		methodLogger.debug(
			`API call completed in ${duration}ms with status: ${response.status} ${response.statusText}`,
			{
				url,
				status: response.status,
			},
		);

		// Check if the response status is OK (2xx)
		if (!response.ok) {
			const errorText = await response.text(); // Get error body for context
			methodLogger.error(`API error response (${response.status}):`, errorText);

			// Use the new error classification system
			throw classifyApiError(
				errorText,
				response.status,
				`API request failed with status ${response.status}: ${response.statusText}`,
			);
		}

		// Attempt to parse the response body as JSON
		try {
			const responseData = await response.json();
			methodLogger.debug("Response body successfully parsed as JSON.");
			// methodLogger.debug('Response Data:', responseData); // Uncomment for full response logging
			return responseData as T;
		} catch (parseError) {
			methodLogger.error("Failed to parse API response JSON:", parseError);
			// Throw a specific error for JSON parsing failure
			throw createApiError(
				`Failed to parse API response JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
				response.status, // Include original status for context
				parseError,
			);
		}
	} catch (error) {
		const endTime = performance.now();
		const duration = (endTime - startTime).toFixed(2);
		methodLogger.error(
			`API call failed after ${duration}ms for ${url}:`,
			error,
		);

		// Rethrow if it's already an McpError (e.g., from status checks or parsing)
		if (error instanceof McpError) {
			throw error;
		}

		// Handle potential network errors (TypeError in fetch)
		if (error instanceof TypeError) {
			throw createNetworkError(
				`Network error during API call: ${error.message}`,
				error,
			);
		}

		// Wrap any other unexpected errors
		throw createUnexpectedError(
			`Unexpected error during API call: ${error instanceof Error ? error.message : String(error)}`,
			error,
		);
	}
}
