import { generateSessionToken } from "@server/auth/sessions/app";
import { db } from "@server/db";
import { Resource, resources, users } from "@server/db";
import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
import { eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import { createResourceSession } from "@server/auth/sessions/resource";
import logger from "@server/logger";
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
import config from "@server/lib/config";
import stoi from "@server/lib/stoi";
import { logAccessAudit } from "#dynamic/lib/logAccessAudit";
import { normalizePostAuthPath } from "@server/lib/normalizePostAuthPath";

const authWithAccessTokenBodySchema = z.strictObject({
    accessToken: z.string(),
    accessTokenId: z.string().optional()
});

const authWithAccessTokenParamsSchema = z.strictObject({
    resourceId: z
        .string()
        .optional()
        .transform(stoi)
        .pipe(z.int().positive().optional())
});

export type AuthWithAccessTokenResponse = {
    session?: string;
    redirectUrl?: string | null;
};

export async function authWithAccessToken(
    req: Request,
    res: Response,
    next: NextFunction
): Promise<any> {
    const parsedBody = authWithAccessTokenBodySchema.safeParse(req.body);

    if (!parsedBody.success) {
        return next(
            createHttpError(
                HttpCode.BAD_REQUEST,
                fromError(parsedBody.error).toString()
            )
        );
    }

    const parsedParams = authWithAccessTokenParamsSchema.safeParse(req.params);

    if (!parsedParams.success) {
        return next(
            createHttpError(
                HttpCode.BAD_REQUEST,
                fromError(parsedParams.error).toString()
            )
        );
    }

    const { resourceId } = parsedParams.data;
    const { accessToken, accessTokenId } = parsedBody.data;

    try {
        let valid;
        let tokenItem;
        let error;
        let resource: Resource | undefined;

        if (accessTokenId) {
            if (!resourceId) {
                return next(
                    createHttpError(
                        HttpCode.BAD_REQUEST,
                        "Resource ID is required"
                    )
                );
            }

            const [foundResource] = await db
                .select()
                .from(resources)
                .where(eq(resources.resourceId, resourceId))
                .limit(1);

            if (!foundResource) {
                return next(
                    createHttpError(HttpCode.NOT_FOUND, "Resource not found")
                );
            }

            const res = await verifyResourceAccessToken({
                accessTokenId,
                accessToken
            });

            valid = res.valid;
            tokenItem = res.tokenItem;
            error = res.error;
            resource = foundResource;
        } else {
            const res = await verifyResourceAccessToken({
                accessToken
            });

            valid = res.valid;
            tokenItem = res.tokenItem;
            error = res.error;
            resource = res.resource;
        }

        if (!tokenItem || !resource) {
            return next(
                createHttpError(
                    HttpCode.UNAUTHORIZED,
                    "Access token does not exist for resource"
                )
            );
        }

        if (!valid) {
            if (config.getRawConfig().app.log_failed_attempts) {
                logger.info(
                    `Resource access token invalid. Resource ID: ${resource.resourceId}. IP: ${req.ip}.`
                );
            }

            logAccessAudit({
                orgId: resource.orgId,
                resourceId: resource.resourceId,
                action: false,
                type: "accessToken",
                userAgent: req.headers["user-agent"],
                requestIp: req.ip
            });

            return next(
                createHttpError(
                    HttpCode.UNAUTHORIZED,
                    error || "Invalid access token"
                )
            );
        }

        const token = generateSessionToken();
        await createResourceSession({
            resourceId: resource.resourceId,
            token,
            accessTokenId: tokenItem.accessTokenId,
            isRequestToken: true,
            expiresAt: Date.now() + 1000 * 30, // 30 seconds
            sessionLength: 1000 * 30,
            doNotExtend: true
        });

        let accessAuditUser: { username: string; userId: string } | undefined;
        if (tokenItem.userId) {
            const [associatedUser] = await db
                .select({
                    userId: users.userId,
                    username: users.username
                })
                .from(users)
                .where(eq(users.userId, tokenItem.userId))
                .limit(1);
            if (associatedUser) {
                accessAuditUser = {
                    userId: associatedUser.userId,
                    username: associatedUser.username
                };
            }
        }

        logAccessAudit({
            orgId: resource.orgId,
            resourceId: resource.resourceId,
            action: true,
            type: "accessToken",
            apiKey: accessAuditUser
                ? undefined
                : {
                      name: tokenItem.title,
                      apiKeyId: tokenItem.accessTokenId
                  },
            user: accessAuditUser,
            metadata: accessAuditUser
                ? {
                      accessTokenId: tokenItem.accessTokenId,
                      accessTokenTitle: tokenItem.title
                  }
                : undefined,
            userAgent: req.headers["user-agent"],
            requestIp: req.ip
        });

        let redirectUrl = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
        const postAuthPath = normalizePostAuthPath(resource.postAuthPath);
        if (tokenItem.path) {
            // add the path from the access token to the redirect URL, ensuring there is exactly one slash between the domain and the path
            redirectUrl =
                redirectUrl.replace(/\/?$/, "/") +
                tokenItem.path.replace(/^\/?/, "");
        } else if (postAuthPath) {
            redirectUrl = redirectUrl + postAuthPath;
        }

        return response<AuthWithAccessTokenResponse>(res, {
            data: {
                session: token,
                redirectUrl
            },
            success: true,
            error: false,
            message: "Authenticated with resource successfully",
            status: HttpCode.OK
        });
    } catch (e) {
        logger.error(e);
        return next(
            createHttpError(
                HttpCode.INTERNAL_SERVER_ERROR,
                "Failed to authenticate with resource"
            )
        );
    }
}
