import { RouterTree } from "@src/lib/routerTree.ts";
import { Context } from "@src/lib/context.ts";
import { Schema } from "@shared/utils/validator.ts";
import { Api } from "@src/lib/apiValidator.ts";
import { notAllowedError, notFoundError } from "@src/lib/errors.ts";
import { err } from "@shared/utils/result.ts";

type RequestHandler<
    S extends string = string,
    ReqSchema extends Schema<unknown> = Schema<unknown>,
    ResSchema extends Schema<unknown> = Schema<unknown>,
> = (c: Context<S, ReqSchema, ResSchema>) => Promise<Response> | Response;

export type Middleware = (
    c: Context<string>,
    next: () => Promise<void>,
) => Promise<Response | void> | Response | void;

type MethodHandler<S extends string> = {
    handler: RequestHandler<S>;
    schema?: { req: Schema<unknown>; res: Schema<unknown> };
};

type MethodHandlers<S extends string = string> = Partial<
    Record<string, MethodHandler<S>>
>;

const DEFAULT_NOT_FOUND_HANDLER =
    (() => new Response("404 Not found", { status: 404 })) as RequestHandler;

const DEFAULT_NOT_ALLOWED_HANDLER =
    ((c) =>
        c.json(err(notAllowedError("405 Not allowed")), {
            status: 405,
        })) as RequestHandler;

class HttpRouter {
    public readonly routerTree = new RouterTree<MethodHandlers>();
    public pathTransformer?: (path: string) => string;
    private middlewares: Middleware[] = [];
    public notFoundHandler: RequestHandler = DEFAULT_NOT_FOUND_HANDLER;
    public methodNotAllowedHandler: RequestHandler =
        DEFAULT_NOT_ALLOWED_HANDLER;

    public setPathTransformer(transformer: (path: string) => string) {
        this.pathTransformer = transformer;
        return this;
    }

    public use(middleware: Middleware): this {
        this.middlewares.push(middleware);
        return this;
    }

    public add<
        S extends string,
        ReqSchema extends Schema<unknown> = Schema<unknown>,
        ResSchema extends Schema<unknown> = Schema<unknown>,
    >(
        path: S,
        method: string,
        handler: RequestHandler<S, ReqSchema, ResSchema>,
        schema?: { req: ReqSchema; res: ResSchema },
    ): HttpRouter;
    public add<
        S extends string,
        ReqSchema extends Schema<unknown> = Schema<unknown>,
        ResSchema extends Schema<unknown> = Schema<unknown>,
    >(
        path: S[],
        method: string,
        handler: RequestHandler<string, ReqSchema, ResSchema>,
        schema?: { req: ReqSchema; res: ResSchema },
    ): HttpRouter;
    public add(
        path: string | string[],
        method: string,
        handler: RequestHandler<string>,
        schema?: { req: Schema<unknown>; res: Schema<unknown> },
    ): HttpRouter {
        const paths = Array.isArray(path) ? path : [path];

        for (const p of paths) {
            this.routerTree.getHandler(p).match(
                (existingHandlers) => {
                    existingHandlers[method] = { handler, schema };
                },
                () => {
                    const newHandlers: MethodHandlers = {};
                    newHandlers[method] = { handler, schema };
                    this.routerTree.add(p, newHandlers);
                },
            );
        }

        return this;
    }

    public get<S extends string>(
        path: S,
        handler: RequestHandler<S>,
    ): HttpRouter;
    public get<S extends string>(
        path: S[],
        handler: RequestHandler,
    ): HttpRouter;
    public get(
        path: string | string[],
        handler: RequestHandler,
    ): HttpRouter {
        if (Array.isArray(path)) {
            return this.add(path, "GET", handler);
        }
        return this.add(path, "GET", handler);
    }

    public post<S extends string>(
        path: S,
        handler: RequestHandler<S>,
    ): HttpRouter;
    public post(
        path: string[],
        handler: RequestHandler,
    ): HttpRouter;
    public post(
        path: string | string[],
        handler: RequestHandler<string>,
    ): HttpRouter {
        if (Array.isArray(path)) {
            return this.add(path, "POST", handler);
        }
        return this.add(path, "POST", handler);
    }

    public api<
        Path extends string,
        ReqSchema extends Schema<unknown>,
        ResSchema extends Schema<unknown>,
    >(
        api: Api<Path, ReqSchema, ResSchema>,
        handler: RequestHandler<Path, ReqSchema, ResSchema>,
    ): HttpRouter {
        return this.add(api.path, api.method, handler, api.schema);
    }

    async handleRequest(
        req: Request,
        connInfo: Deno.ServeHandlerInfo<Deno.Addr>,
    ): Promise<Response> {
        let ctx = new Context(req, connInfo, {});
        const path = this.pathTransformer
            ? this.pathTransformer(ctx.path)
            : ctx.path;

        const { handler, params } = this.resolveRoute(ctx, path);
        ctx = ctx.setParams(params);

        const res =
            (await this.runMiddlewares(this.middlewares, handler, ctx)).res;
        return req.method === "HEAD" ? this.removeBodyFromResponse(res) : res;
    }

    private resolveRoute(
        ctx: Context,
        path: string,
    ): { handler: RequestHandler; params: Record<string, string> } {
        const routeOption = this.routerTree.find(path);

        if (routeOption.isSome()) {
            const { value: methodHandlers, params } = routeOption.value;
            let route = methodHandlers[ctx.req.method];

            if (!route && ctx.req.method === "HEAD") {
                route = methodHandlers["GET"];
            } else if (!route && ctx.req.method !== "GET") {
                if (ctx.preferredType.map((v) => v === "json").toBoolean()) {
                    return { handler: this.methodNotAllowedHandler, params };
                }
            }
            if (route) {
                if (route.schema) {
                    ctx = ctx.setSchema(route.schema);
                }
                return { handler: route.handler, params };
            }
        }
        return { handler: this.notFoundHandler, params: {} };
    }

    private removeBodyFromResponse(res: Response): Response {
        const headers = new Headers(res.headers);
        headers.set("Content-Length", "0");
        return new Response(null, {
            headers,
            status: res.status,
            statusText: res.statusText,
        });
    }

    private async runMiddlewares(
        middlewares: Middleware[],
        handler: RequestHandler,
        ctx: Context,
    ) {
        const dispatch = async (index: number): Promise<void> => {
            if (index < middlewares.length) {
                const middleware = middlewares[index];
                const result = await middleware(ctx, () => dispatch(index + 1));
                if (result !== undefined) {
                    ctx.res = await Promise.resolve(result);
                }
            } else {
                ctx.res = await handler(ctx);
            }
        };
        await dispatch(0);
        return ctx;
    }

    private buildNotFoundHandler(c: Context) {
        return c.matchPreferredType(
            () => c.html("404 Not found", { status: 404 }),
            () => c.json(err(notFoundError("404 Not found")), { status: 404 }),
            () => new Response("404 Not found", { status: 404 }),
        );
    }
}

export type ExtractRouteParams<T extends string> = T extends string
    ? T extends `${infer _Start}:${infer Param}/${infer Rest}`
        ? Param | ExtractRouteParams<Rest>
    : T extends `${infer _Start}:${infer Param}` ? Param
    : T extends `${infer _Start}*` ? "restOfThePath"
    : never
    : never;

export type Params<Keys extends string> = {
    [K in Keys]: string;
};

export default HttpRouter;
