import { zValidator } from "@hono/zod-validator";
import { eq } from "drizzle-orm";
import { Hono } from "hono";
import { env } from "hono/adapter";
import { z } from "zod";
import { appRequestInsertSchema, appRequests, appResponseInsertSchema, appResponses, appRoutes, } from "../db/schema.js";
import { errorToJson, generateUUID } from "../lib/utils.js";
const app = new Hono();
app.get("/v0/app-routes", async (ctx) => {
    const db = ctx.get("db");
    const routes = await db.select().from(appRoutes);
    const baseUrl = env(ctx).MIZU_SERVICE_TARGET ?? "http://localhost:8787";
    return ctx.json({
        baseUrl,
        routes,
    });
});
app.get("/v0/all-requests", async (ctx) => {
    const db = ctx.get("db");
    const requests = await db
        .select()
        .from(appResponses)
        .rightJoin(appRequests, eq(appResponses.requestId, appRequests.id));
    return ctx.json(requests);
});
app.post("/v0/send-request", zValidator("json", appRequestInsertSchema), async (ctx) => {
    const traceId = ctx.req.header("x-fpx-trace-id") ?? generateUUID();
    const { requestMethod, requestUrl, requestHeaders, requestQueryParams, requestPathParams, requestBody, requestRoute, } = ctx.req.valid("json");
    const db = ctx.get("db");
    // NOTE - We want to pass the trace-id through to the service we're calling
    //        This helps us optionally correlate requests more easily in the frontend
    const modifiedRequestHeaders = {
        ...(requestHeaders ?? {}),
        "x-fpx-trace-id": traceId,
    };
    const requestObject = {
        method: requestMethod,
        body: (requestBody ? JSON.stringify(requestBody) : requestBody) ??
            // biome-ignore lint/suspicious/noExplicitAny: just make it work
            undefined,
        headers: modifiedRequestHeaders,
    };
    const finalUrl = resolveUrl(requestUrl, requestQueryParams);
    const newRequest = {
        requestMethod,
        requestUrl,
        requestHeaders,
        requestPathParams,
        requestQueryParams,
        requestBody,
        requestRoute,
    };
    const insertResult = await db
        .insert(appRequests)
        .values(newRequest)
        .returning({ requestId: appRequests.id });
    const requestId = insertResult[0].requestId; // only one insert always happens not sure why drizzle returns an array...
    const startTime = Date.now();
    try {
        const response = await fetch(finalUrl, requestObject);
        const duration = Date.now() - startTime;
        const { responseStatusCode, responseTime, responseHeaders, responseBody, traceId: responseTraceId, isFailure, } = await handleSuccessfulRequest(db, requestId, duration, response);
        if (responseTraceId !== traceId) {
            console.warn(`Trace-id mismatch! Request: ${traceId}, Response: ${responseTraceId}`);
        }
        return ctx.json({
            responseStatusCode,
            responseTime,
            responseHeaders,
            responseBody,
            isFailure,
            traceId: responseTraceId,
            requestId,
        });
    }
    catch (fetchError) {
        // NOTE - This will happen when the service is unreachable.
        //        Could be good to parse the error for more information!
        const responseTime = Date.now() - startTime;
        const { failureDetails, failureReason, isFailure } = await handleFailedRequest(db, requestId, traceId, responseTime, fetchError);
        return ctx.json({
            isFailure,
            responseTime,
            failureDetails,
            failureReason,
            traceId,
            requestId,
        });
    }
});
/**
 * Extract useful data from the response when a request succeeds,
 * and save that data in the `app_responses` table
 */
async function handleSuccessfulRequest(db, requestId, duration, response) {
    const traceId = response.headers.get("x-fpx-trace-id") ?? "";
    const { responseBody, responseTime, responseHeaders, responseStatusCode } = await appResponseInsertSchema
        .extend({
        headers: z.instanceof(Headers),
        status: z.number(),
        body: z.instanceof(ReadableStream),
        traceId: z.string().optional(),
    })
        .transform(async ({ headers, status }) => {
        const responseHeaders = {};
        // NOTE - Order of arguments when you do `forEach` on a Headers object is (headerValue, headerName)
        headers.forEach((headerValue, headerName) => {
            responseHeaders[headerName] = headerValue;
        });
        return {
            responseHeaders,
            responseStatusCode: status,
            responseBody: await safeReadTextBody(response),
            responseTime: duration,
        };
    })
        .parseAsync(response);
    await db.insert(appResponses).values([
        {
            isFailure: false,
            responseStatusCode,
            responseTime,
            responseHeaders,
            responseBody,
            traceId,
            requestId,
        },
    ]);
    return {
        isFailure: false,
        responseStatusCode,
        responseTime,
        responseHeaders,
        responseBody,
        traceId,
    };
}
function safeReadTextBody(response) {
    return response.text().catch((error) => {
        console.error("Failed to parse response body", error);
        return null;
    });
}
function hasMessage(error) {
    return (!!error &&
        typeof error === "object" &&
        "message" in error &&
        typeof error?.message === "string");
}
/**
 * Extract useful data from the error when a request fails,
 * and save that data in the `app_responses` table
 */
async function handleFailedRequest(db, requestId, traceId, responseTime, error) {
    let failureReason = "unknown";
    if (hasMessage(error)) {
        failureReason = error.message;
    }
    let failureDetails = {};
    if (error instanceof Error) {
        failureDetails = errorToJson(error);
    }
    await db.insert(appResponses).values([
        {
            isFailure: true,
            responseTime,
            traceId,
            requestId,
            failureReason,
            failureDetails,
        },
    ]);
    return {
        isFailure: true,
        responseTime,
        traceId,
        requestId,
        failureReason,
        failureDetails,
    };
}
function resolveUrl(url, queryParams) {
    if (!queryParams)
        return url;
    const urlObject = new URL(url);
    for (const [key, value] of Object.entries(queryParams)) {
        urlObject.searchParams.set(key, value);
    }
    return urlObject.toString();
}
export default app;
