import {
    InferSchemaType,
    Schema,
    SchemaValidationError,
} from "@shared/utils/validator.ts";
import {
    RequestValidationError,
    requestValidationError,
    ResponseValidationError,
    responseValidationError,
} from "@src/lib/errors.ts";
import { ResultAsync } from "@shared/utils/resultasync.ts";

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
    : never
    : never;

type ApiError =
    | SchemaValidationError
    | ResponseValidationError;

export class Api<
    Path extends string,
    ReqSchema extends Schema<any>,
    ResSchema extends Schema<any>,
> {
    private readonly pathSplitted: string[];
    private readonly paramIndexes: Record<string, number>;

    constructor(
        public readonly path: Path,
        public readonly method: string,
        public readonly schema: {
            req: ReqSchema;
            res: ResSchema;
        },
    ) {
        this.pathSplitted = path.split("/");
        this.paramIndexes = this.pathSplitted.reduce<Record<string, number>>(
            (acc, segment, index) => {
                if (segment.startsWith(":")) {
                    acc[segment.slice(1)] = index;
                }
                return acc;
            },
            {},
        );
    }

    makeRequest(
        reqBody: InferSchemaType<ReqSchema>,
        params: { [K in ExtractRouteParams<Path>]: string },
    ): ResultAsync<InferSchemaType<ResSchema>, ApiError> {
        return this.schema.req
            .parse(reqBody)
            .toAsync()
            .mapErr((e) => requestValidationError(e.msg))
            .andThenAsync(async (data) => {
                const pathSplitted = this.pathSplitted;
                for (const [key, value] of Object.entries(params)) {
                    pathSplitted[this.paramIndexes[key]] = value as string;
                }
                const path = pathSplitted.join("/");

                const response = await fetch(
                    path,
                    {
                        method: this.method,
                        headers: {
                            "Content-Type": "application/json",
                        },
                        body: JSON.stringify(data),
                    },
                );

                const resBody = await response.json();

                return this.schema.res.parse(resBody).toAsync()
                    .map((v) => v as InferSchemaType<ResSchema>)
                    .mapErr((e) => responseValidationError(e.msg));
            });
    }

    public makeSafeRequest(
        reqBody: InferSchemaType<ReqSchema>,
        params: { [K in ExtractRouteParams<Path>]: string },
    ): ResultAsync<InferSchemaType<ResSchema>, ResponseValidationError> {
        return this.makeRequest(reqBody, params).mapErr((e) => {
            if (e.type === "RequestValidationError") {
                throw "Failed to validate request";
            }
            return e;
        });
    }
}
