import { RouterTree } from "@lib/routerTree.ts";
import { none, Option, some } from "@shared/utils/option.ts";
import { Context } from "@lib/context.ts";
import { Schema } from "@shared/utils/validator.ts";
import { Api } from "@src/lib/apiValidator.ts";

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

type RequestHandlerWithSchema<S extends string> = {
    handler: RequestHandler<S>;
    schema?: {
        res: Schema<any>;
        req: Schema<any>;
    };
};
export type Middleware = (
    c: Context<string>,
    next: () => Promise<void>,
) => Promise<Response | void> | Response | void;

type MethodHandlers<S extends string> = Partial<
    Record<string, {
        handler: RequestHandler<S>;
        schema?: {
            res: Schema<any>;
            req: Schema<any>;
        };
    }>
>;

const DEFAULT_NOT_FOUND_HANDLER = () => new Response("404 Not found");

class HttpRouter {
    public readonly routerTree = new RouterTree<MethodHandlers<any>>();
    public pathPreprocessor?: (path: string) => string;
    private middlewares: Middleware[] = [];
    public defaultNotFoundHandler: RequestHandler<string> =
        DEFAULT_NOT_FOUND_HANDLER;

    public setPathProcessor(processor: (path: string) => string) {
        this.pathPreprocessor = processor;
        return this;
    }

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

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

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

        return this;
    }

    public get<S extends string>(
        path: S,
        handler: RequestHandler<S>,
    ): HttpRouter;
    public get<S extends string>(
        path: S[],
        handler: RequestHandler<string>,
    ): HttpRouter;
    public get(
        path: string | string[],
        handler: RequestHandler<string>,
    ): 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<S extends string>(
        path: string[],
        handler: RequestHandler<string>,
    ): 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<any>,
        ResSchema extends Schema<any>,
    >(
        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, {});
        let routeParams: Record<string, string> = {};
        const path = this.pathPreprocessor
            ? this.pathPreprocessor(ctx.path)
            : ctx.path;

        const handler = this.routerTree
            .find(path)
            .andThen((match) => {
                const { value: methodHandler, params: params } = match;
                routeParams = params;
                const route = methodHandler[req.method];
                if (!route) return none;
                if (route.schema) {
                    ctx = ctx.withSchema(route.schema);
                }
                const handler = route.handler;

                return some(handler);
            })
            .unwrapOrElse(() => this.defaultNotFoundHandler);

        const res = (await this.executeMiddlewareChain(
            this.middlewares,
            handler,
            Context.setParams(ctx, routeParams),
        )).res;

        return res;
    }

    private async executeMiddlewareChain<S extends string>(
        middlewares: Middleware[],
        handler: RequestHandler<S>,
        c: Context<S>,
    ) {
        let currentIndex = -1;

        const dispatch = async (index: number): Promise<void> => {
            currentIndex = index;

            if (index < middlewares.length) {
                const middleware = middlewares[index];

                const result = await middleware(c, () => dispatch(index + 1));

                if (result !== undefined) {
                    c.res = await Promise.resolve(result);
                }
            } else {
                const res = await handler(c);
                c.res = res;
            }
        };

        await dispatch(0);

        return c;
    }

    private setParams(path: string, params: string[]): Params<string> {
        path.split("/").filter((segmet) => segmet.startsWith(":"));
    }
}

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;
