import { STATUS_CODE } from '@std/http';

import { OrPromise } from './util.ts';

export type Method = string;

export const Method = {
	GET: 'GET',
	POST: 'POST',
	PUT: 'PUT',
	PATCH: 'PATCH',
	Any: '___INTERNAL_ANY',
};

export type Handler = (
	request: Request,
	pathParams: Record<string, string>,
) => OrPromise<Response>;

export type Match = string | URLPattern;
export class Router {
	#pathnameCache: Map<string, URLPattern>;
	#routes: Map<URLPattern, Map<Method, Handler>>;

	constructor() {
		this.#routes = new Map();
		this.#pathnameCache = new Map();
	}

	register(method: Method, match: Match, handler: Handler) {
		const urlPattern = (match instanceof URLPattern) ? match : this.#cachePathname(match);
		if (!this.#routes.has(urlPattern)) {
			this.#routes.set(urlPattern, new Map());
		}

		const methodMap = this.#routes.get(urlPattern)!;
		method = method.toUpperCase();
		methodMap.set(method, handler);
	}

	#cachePathname(pathname: string) {
		const cached = this.#pathnameCache.get(pathname);
		if (cached) {
			return cached;
		}

		const pattern = new URLPattern({ pathname });
		this.#pathnameCache.set(pathname, pattern);

		return pattern;
	}

	async handle(req: Request): Promise<Response> {
		const method = req.method.toUpperCase();

		for (const [pattern, methodMap] of this.#routes.entries()) {
			const match = pattern.exec(req.url);
			if (!match) {
				continue;
			}

			let handler = methodMap.get(method);
			if (!handler) {
				handler = methodMap.get(Method.Any);
			}
			if (!handler) {
				return new Response('Method not allowed', {
					status: STATUS_CODE.MethodNotAllowed,
				});
			}

			return handler(req, match.pathname.groups as Record<string, string>);
		}

		return new Response('Not found', { status: STATUS_CODE.NotFound });
	}
}
