module api_mapping;

import handy_http_primitives;
import handy_http_handlers.path_handler;
import handy_http_handlers.filtered_handler;
import slf4d;

/// The base path to all API endpoints.
private const API_PATH = "/api";

/**
 * Defines the Finnow API mapping with a main PathHandler.
 * Params:
 *   webOrigin = The origin to use when configuring CORS headers.
 * Returns: The handler to plug into an HttpServer.
 */
HttpRequestHandler mapApiHandlers(string webOrigin) {
	PathHandler h = new PathHandler();

	// Generic, public endpoints:
	h.map(HttpMethod.GET, "/status", &getStatus);
	h.map(HttpMethod.OPTIONS, "/**", &getOptions);

	// Dev endpoint for sample data: REMOVE BEFORE DEPLOYING!!!
	// h.map(HttpMethod.POST, "/sample-data", &sampleDataEndpoint);

	// Auth endpoints:
	import auth.api;
	h.map(HttpMethod.POST, "/login", &postLogin);
	h.map(HttpMethod.POST, "/register", &postRegister);
	h.map(HttpMethod.GET, "/register/username-availability", &getUsernameAvailability);



	// Authenticated endpoints:
	PathHandler a = new PathHandler();
	a.map(HttpMethod.GET, "/me", &getMyUser);
	a.map(HttpMethod.DELETE, "/me", &deleteMyUser);
	a.map(HttpMethod.GET, "/me/token", &getNewToken);
	a.map(HttpMethod.POST, "/me/password", &changeMyPassword);

	import profile.api;
	a.map(HttpMethod.GET, "/profiles", &handleGetProfiles);
	a.map(HttpMethod.POST, "/profiles", &handleCreateNewProfile);
	/// URL path to a specific profile, with the :profile path parameter.
	const PROFILE_PATH = "/profiles/:profile";
	a.map(HttpMethod.GET, PROFILE_PATH, &handleGetProfile);
	a.map(HttpMethod.DELETE, PROFILE_PATH, &handleDeleteProfile);
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/properties", &handleGetProperties);
	import attachment.api;
	// Note: the download endpoint is public! We authenticate via token in query params instead of header here.
	h.map(HttpMethod.GET, PROFILE_PATH ~ "/attachments/:attachmentId/download", &handleDownloadAttachment);

	// Account endpoints:
	import account.api;
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/accounts", &handleGetAccounts);
	a.map(HttpMethod.POST, PROFILE_PATH ~ "/accounts", &handleCreateAccount);
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/account-balances", &handleGetTotalBalances);
	const ACCOUNT_PATH = PROFILE_PATH ~ "/accounts/:accountId:ulong";
	a.map(HttpMethod.GET, ACCOUNT_PATH, &handleGetAccount);
	a.map(HttpMethod.PUT, ACCOUNT_PATH, &handleUpdateAccount);
	a.map(HttpMethod.DELETE, ACCOUNT_PATH, &handleDeleteAccount);
	a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/balance", &handleGetAccountBalance);
	a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/history", &handleGetAccountHistory);
	a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records", &handleGetValueRecords);
	a.map(HttpMethod.GET, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong", &handleGetValueRecord);
	a.map(HttpMethod.POST, ACCOUNT_PATH ~ "/value-records", &handleCreateValueRecord);
	a.map(HttpMethod.DELETE, ACCOUNT_PATH ~ "/value-records/:valueRecordId:ulong", &handleDeleteValueRecord);

	import transaction.api;
	// Transaction vendor endpoints:
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/vendors", &handleGetVendors);
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/vendors/:vendorId:ulong", &handleGetVendor);
	a.map(HttpMethod.POST, PROFILE_PATH ~ "/vendors", &handleCreateVendor);
	a.map(HttpMethod.PUT, PROFILE_PATH ~ "/vendors/:vendorId:ulong", &handleUpdateVendor);
	a.map(HttpMethod.DELETE, PROFILE_PATH ~ "/vendors/:vendorId:ulong", &handleDeleteVendor);
	// Transaction category endpoints:
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/categories", &handleGetCategories);
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/categories/:categoryId:ulong", &handleGetCategory);
	a.map(HttpMethod.POST, PROFILE_PATH ~ "/categories", &handleCreateCategory);
	a.map(HttpMethod.PUT, PROFILE_PATH ~ "/categories/:categoryId:ulong", &handleUpdateCategory);
	a.map(HttpMethod.DELETE, PROFILE_PATH ~ "/categories/:categoryId:ulong", &handleDeleteCategory);
	// Transaction endpoints:
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/transactions", &handleGetTransactions);
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/transactions/search", &handleSearchTransactions);
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/transactions/:transactionId:ulong", &handleGetTransaction);
	a.map(HttpMethod.POST, PROFILE_PATH ~ "/transactions", &handleAddTransaction);
	a.map(HttpMethod.PUT, PROFILE_PATH ~ "/transactions/:transactionId:ulong", &handleUpdateTransaction);
	a.map(HttpMethod.DELETE, PROFILE_PATH ~ "/transactions/:transactionId:ulong", &handleDeleteTransaction);
	
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/transaction-tags", &handleGetAllTags);

	// Analytics endpoints:
	import analytics.api;
	a.map(HttpMethod.GET, PROFILE_PATH ~ "/analytics/balance-time-series", &handleGetBalanceTimeSeries);

	import data_api;
	// Various other data endpoints:
	a.map(HttpMethod.GET, "/currencies", &handleGetCurrencies);

	// Protect all authenticated paths with a filter.
	import auth.service : AuthenticationFilter;
	HttpRequestFilter authenticationFilter = new AuthenticationFilter();
	h.addMapping(API_PATH ~ "/**", new FilteredHandler(
			[authenticationFilter],
			a
	));

	// Build the main handler into a filter chain:
	return new FilteredHandler(
		[
			cast(HttpRequestFilter) new CorsFilter(webOrigin),
			cast(HttpRequestFilter) new ContentLengthFilter(),
			cast(HttpRequestFilter) new ExceptionHandlingFilter()
		],
		h
	);
}

private void getStatus(ref ServerHttpRequest request, ref ServerHttpResponse response) {
	response.writeBodyString("online", ContentTypes.TEXT_PLAIN);
}

private void getOptions(ref ServerHttpRequest request, ref ServerHttpResponse response) {
	// Do nothing, just return 200 OK.
}

private void sampleDataEndpoint(ref ServerHttpRequest request, ref ServerHttpResponse response) {
	import slf4d;
	import util.sample_data;
	import core.thread;
	Thread t = new Thread(() {
		try {
			generateSampleData();
		} catch (Exception e) {
			error("Error while generating sample data.", e);
		}
	});
	t.start();
	info("Started new thread to generate sample data.");
}

private void map(
	PathHandler handler,
	HttpMethod method,
	string subPath,
	void function(ref ServerHttpRequest, ref ServerHttpResponse) fn
) {
	handler.addMapping(method, API_PATH ~ subPath, HttpRequestHandler.of(fn));
}

private class CorsFilter : HttpRequestFilter {
	private string webOrigin;

	this(string webOrigin) {
		this.webOrigin = webOrigin;
	}
	
	void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) {
		response.headers.add("Access-Control-Allow-Origin", webOrigin);
		response.headers.add("Access-Control-Allow-Methods", "*");
		response.headers.add("Access-Control-Allow-Headers", "Authorization, Content-Type");
		filterChain.doFilter(request, response);
	}
}

private class ContentLengthFilter : HttpRequestFilter {
	const MAX_LENGTH = 1024 * 1024 * 20; // 2MB limit

	void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) {
		if ("Content-Length" in request.headers) {
			ulong contentLength = request.getHeaderAs!ulong("Content-Length");
			if (contentLength > MAX_LENGTH) {
				warnF!"Received request with content length of %d, larger than max allowed %d bytes."(
					contentLength,
					MAX_LENGTH
				);
				import std.conv;
				response.status = HttpStatus.PAYLOAD_TOO_LARGE;
				response.writeBodyString(
					"Request body is too large. Must be at most " ~ MAX_LENGTH.to!string ~ " bytes."
				);
				return; // Don't propagate the filter.
			}
		}

		filterChain.doFilter(request, response);
	}
}

private class ExceptionHandlingFilter : HttpRequestFilter {
	void doFilter(ref ServerHttpRequest request, ref ServerHttpResponse response, FilterChain filterChain) {
		try {
			filterChain.doFilter(request, response);
		} catch (HttpStatusException e) {
			response.status = e.status;
			response.writeBodyString(e.message.idup);
		} catch (Exception e) {
			error(e);
			response.status = HttpStatus.INTERNAL_SERVER_ERROR;
			response.writeBodyString("An error occurred: " ~ e.msg);
		}
	}
}
