/**
 * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
 * 1. You want to modify request context (see Part 1).
 * 2. You want to create a new middleware or type of procedure (see Part 3).
 *
 * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
 * need to use are documented accordingly near the end.
 */

import { initTRPC, TRPCError } from "@trpc/server";
import type { CreateNextContextOptions } from "@trpc/server/adapters/next";
import superjson from "superjson";
import z, { ZodError } from "zod/v4";
import { env } from "~/env";
import { verifyToken } from "~/server/auth/jwt";
import { zUserId } from "~/server/utils/zod_types";
import { TOKEN_CONFIG } from "../auth/configs";

/**
 * 1. CONTEXT
 *
 * This section defines the "contexts" that are available in the backend API.
 *
 * These allow you to access things when processing a request, like the database, the session, etc.
 */
export const sessionSchema = z.object({
	email: z.email(),
	id: zUserId,
	isAdmin: z.boolean(),
	isBlocked: z.boolean(),
	nome: z.string(),
	username: z.string(),
	mustChangePassword: z.boolean(),
});

export type Session = z.infer<typeof sessionSchema>;

export type SessionStatus =
	| "AUTHENTICATED"
	| "EXPIRED"
	| "INVALID"
	| "NO_SESSION";

interface CreateInnerContextOptions extends Partial<CreateNextContextOptions> {
	accessToken?: string;
}
export const createInnerTRPCContext = async ({
	accessToken,
	...opts
}: CreateInnerContextOptions) => {
	let sessionData: Session | null = null;
	let sessionStatus: SessionStatus = "NO_SESSION";

	if (accessToken) {
		const { expired, session } = await verifyToken(accessToken);
		if (expired) {
			sessionStatus = "EXPIRED";
		} else if (session) {
			sessionData = session;
			sessionStatus = "AUTHENTICATED";
		} else {
			sessionStatus = "INVALID";
		}
	}

	return {
		...opts,
		session: sessionData,
		sessionStatus,
	};
};

export const createTRPCContext = async ({
	req,
	res,
}: Partial<CreateNextContextOptions>) => {
	if (!req || !res) {
		throw new Error(
			"You are missing `req` or `res` in your call to createTRPCContext.",
		);
	}
	const accessToken = req.cookies[TOKEN_CONFIG.ACCESS_TOKEN_COOKIE_NAME];

	return await createInnerTRPCContext({ accessToken, req, res });
};
export type Context = Awaited<ReturnType<typeof createTRPCContext>>;
export const addApiAuthToContext = (ctx: Context) => {
	if (!ctx.req || !ctx.res) {
		throw new Error(
			"You are missing `req` or `res` in your call to addApiAuthToContext.",
		);
	}
	const authString = Buffer.from(
		`${env.EXP_API_USER}:${env.EXP_API_PASS}`,
	).toString("base64");
	ctx.req.headers.authorization = `Basic ${authString}`;
	return ctx;
};

/**
 * 2. INITIALIZATION
 *
 * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
 * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
 * errors on the backend.
 */

export const t = initTRPC.context<Context>().create({
	errorFormatter({ shape, error }) {
		return {
			...shape,
			data: {
				...shape.data,
				zodError:
					error.cause instanceof ZodError ? z.treeifyError(error.cause) : null,
			},
		};
	},
	sse: {
		client: {
			reconnectAfterInactivityMs: 5_000,
		},
		//maxDurationMs: 5 * 60 * 1_000, // 5 minutes
		ping: {
			enabled: true,
			intervalMs: 3_000,
		},
	},
	transformer: superjson,
});

/**
 * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
 *
 * These are the pieces you use to build your tRPC API. You should import these a lot in the
 * "/src/server/api/routers" directory.
 */

/**
 * This is how you create new routers and sub-routers in your tRPC API.
 *
 * @see https://trpc.io/docs/router
 */
export const createTRPCRouter = t.router;

/**
 * Public (unauthenticated) procedure
 *
 * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
 * guarantee that a user querying is authorized, but you can still access user session data if they
 * are logged in.
 */
export const publicProcedure = t.procedure;

/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceUserIsAuthed = t.middleware(({ ctx, next, path }) => {
	if (ctx.sessionStatus === "EXPIRED") {
		throw new TRPCError({
			code: "UNAUTHORIZED",
			message: `SESSION_EXPIRED, error in path: ${path} [${ctx.req?.url}]`,
		});
	}
	if (!ctx.session) {
		throw new TRPCError({
			code: "UNAUTHORIZED",
			message: `User is not logged, error in path: ${path} [${ctx.req?.url}]`,
		});
	}

	return next({
		ctx: {
			// infers the `session` as non-nullable
			session: { ...ctx.session },
		},
	});
});

const enforceUserIsAuthedOrOverride = t.middleware(({ ctx, next, path }) => {
	const overrideToken =
		ctx.req?.cookies[TOKEN_CONFIG.OVERRIDE_TOKEN_COOKIE_NAME];
	if (overrideToken && overrideToken === env.EXP_API_PASS) {
		return next({
			ctx: {
				// infers the `session` as non-nullable
				session: { ...ctx.session },
			},
		});
	}
	if (!ctx.session) {
		throw new TRPCError({
			code: "UNAUTHORIZED",
			message: `User is not logged, error in path: ${path} [${ctx.req?.url}]`,
		});
	}

	return next({
		ctx: {
			// infers the `session` as non-nullable
			session: { ...ctx.session },
		},
	});
});

/**
 * Protected (authenticated) procedure
 *
 * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
 * the session is valid and guarantees `ctx.session.user` is not null.
 *
 * @see https://trpc.io/docs/procedures
 */
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
export const overridableProcedure = t.procedure.use(
	enforceUserIsAuthedOrOverride,
);

/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceUserIsAdmin = t.middleware(({ ctx, next, path }) => {
	if (ctx.sessionStatus === "EXPIRED") {
		throw new TRPCError({
			code: "UNAUTHORIZED",
			message: `SESSION_EXPIRED, error in path: ${path} [${ctx.req?.url}]`,
		});
	}
	if (!ctx.session?.isAdmin) {
		throw new TRPCError({
			code: "UNAUTHORIZED",
			message: `User is not Admin, error in path: ${path} [${ctx.req?.url}]`,
		});
	}
	return next({
		ctx: {
			// infers the `session` as non-nullable
			session: { ...ctx.session },
		},
	});
});

/**
 * Protected (authenticated) procedure
 *
 * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
 * the session is valid and guarantees `ctx.session.user` is not null.
 *
 * @see https://trpc.io/docs/procedures
 */
export const adminProcedure = t.procedure.use(enforceUserIsAdmin);

export const apiProcedure = publicProcedure.use((opts) => {
	if (!opts.ctx.req || !opts.ctx.res) {
		throw new Error("You are missing `req` or `res` in your call.");
	}
	return opts.next({
		ctx: {
			// We overwrite the context with the truthy `req` & `res`, which will also overwrite the types used in your procedure.
			req: opts.ctx.req,
			res: opts.ctx.res,
		},
	});
});

const enforceProtectedApi = t.middleware(async ({ ctx, next, path }) => {
	if (!ctx.req || !ctx.res) {
		throw new TRPCError({
			code: "UNAUTHORIZED",
			message: `No request or response object, error in path: ${path} [${ctx.req?.url}]`,
		});
	}
	const basicAuth = ctx.req.headers.authorization;
	if (!basicAuth) {
		throw new TRPCError({
			code: "UNAUTHORIZED",
			message: `No Auth header found: ${path} [${ctx.req?.url}]`,
		});
	}

	const authValue = basicAuth.split(" ")[1];

	if (!authValue) {
		throw new TRPCError({
			code: "UNAUTHORIZED",
			message: `Auth value invalid: ${path} [${ctx.req?.url}]`,
		});
	}

	const [user, pwd] = atob(authValue).split(":");

	if (user !== env.EXP_API_USER || pwd !== env.EXP_API_PASS) {
		throw new TRPCError({
			code: "UNAUTHORIZED",
			message: `User or Secret not valid: ${path} [${ctx.req?.url}]`,
		});
	}
	return next({
		ctx: {
			// infers the `session` as non-nullable
			req: ctx.req,
			res: ctx.res,
		},
	});
});

export const protectedApiProcedure = publicProcedure.use(enforceProtectedApi);
