import { NextRequest, NextResponse } from "next/server";
import { prisma } from "../../../../../lib/server/db";
import {
  getSessionPepper,
  isDatabaseConfigured,
} from "../../../../../lib/server/env";
import { hashSessionToken } from "../../../../../lib/server/hash";
import {
  createSessionForUser,
  getValidatedSessionTokenHashForUser,
  setSessionCookie,
} from "../../../../../lib/server/session";
import { dbUnavailable } from "../../../../../lib/server/responses";
import {
  REQUEST_ID_HEADER,
  getOrCreateRequestId,
  logRouteError,
} from "../../../../../lib/server/requestId";
import { getPublicOrigin } from "../../../../../lib/server/publicOrigin";

const SCOPE = "user.emailChange.verify";

export async function GET(request: NextRequest) {
  const requestId = getOrCreateRequestId(request);

  if (!isDatabaseConfigured()) {
    const res = dbUnavailable();
    res.headers.set(REQUEST_ID_HEADER, requestId);
    return res;
  }

  try {
    const token = request.nextUrl.searchParams.get("token");
    if (!token || token.length < 10) {
      return redirectWithRequestId(
        request,
        "/profile?error=email_change_invalid",
        requestId,
      );
    }

    let pepper: string;
    try {
      pepper = getSessionPepper();
    } catch (err) {
      logRouteError(SCOPE, requestId, err, { phase: "getSessionPepper" });
      return redirectWithRequestId(
        request,
        "/profile?error=email_change_server",
        requestId,
      );
    }

    const tokenHash = hashSessionToken(token, pepper);
    const row = await prisma.emailChangeToken.findUnique({
      where: { tokenHash },
    });

    if (!row || row.expiresAt < new Date()) {
      return redirectWithRequestId(
        request,
        "/profile?error=email_change_expired",
        requestId,
      );
    }

    const keepSessionTokenHash = await getValidatedSessionTokenHashForUser(
      row.userId,
    );

    try {
      await prisma.$transaction(async (tx) => {
        const claim = await tx.emailChangeToken.findUnique({
          where: { id: row.id },
        });
        if (!claim || claim.expiresAt < new Date()) {
          throw Object.assign(new Error("expired"), { __expired: true });
        }

        const taken = await tx.user.findFirst({
          where: {
            email: claim.newEmail,
            NOT: { id: claim.userId },
          },
        });
        if (taken) {
          await tx.emailChangeToken.delete({ where: { id: claim.id } });
          throw Object.assign(new Error("taken"), { __taken: true });
        }

        await tx.user.update({
          where: { id: claim.userId },
          data: { email: claim.newEmail },
        });
        await tx.emailChangeToken.delete({ where: { id: claim.id } });

        if (keepSessionTokenHash) {
          await tx.session.deleteMany({
            where: {
              userId: claim.userId,
              tokenHash: { not: keepSessionTokenHash },
            },
          });
        } else {
          await tx.session.deleteMany({
            where: { userId: claim.userId },
          });
        }
      });
    } catch (err: unknown) {
      if (
        err &&
        typeof err === "object" &&
        "__taken" in err &&
        (err as { __taken?: boolean }).__taken
      ) {
        return redirectWithRequestId(
          request,
          "/profile?error=email_change_taken",
          requestId,
        );
      }
      if (
        err &&
        typeof err === "object" &&
        "__expired" in err &&
        (err as { __expired?: boolean }).__expired
      ) {
        return redirectWithRequestId(
          request,
          "/profile?error=email_change_expired",
          requestId,
        );
      }
      logRouteError(SCOPE, requestId, err, { phase: "transaction" });
      return redirectWithRequestId(
        request,
        "/profile?error=email_change_server",
        requestId,
      );
    }

    if (!keepSessionTokenHash) {
      const { token: sessionToken, expiresAt } = await createSessionForUser(
        row.userId,
      );
      await setSessionCookie(sessionToken, expiresAt);
    }

    return redirectWithRequestId(
      request,
      "/profile?email_change=ok",
      requestId,
    );
  } catch (err) {
    logRouteError(SCOPE, requestId, err);
    return redirectWithRequestId(
      request,
      "/profile?error=email_change_server",
      requestId,
    );
  }
}

function redirectWithRequestId(
  request: NextRequest,
  path: string,
  requestId: string,
): NextResponse {
  const res = NextResponse.redirect(new URL(path, getPublicOrigin(request)));
  res.headers.set(REQUEST_ID_HEADER, requestId);
  return res;
}
