import { randomBytes } from "node:crypto";
import {
  PASSWORD_MAX_LENGTH,
  PASSWORD_MIN_LENGTH,
  PASSWORD_POLICY_MESSAGE,
  checkPasswordPolicy,
} from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { createTRPCRouter, publicProcedure } from "../trpc.js";
import { getAppBaseUrl } from "../lib/app-base-url.js";
import { sendEmail } from "../lib/email.js";
import { authRateLimiter } from "../middleware/rate-limit.js";

const RESET_TTL_MS = 60 * 60 * 1000; // 1 hour

function resetEmailHtml(resetUrl: string): string {
  return `
    <p>You requested a password reset for your CapaKraken account.</p>
    <p>Click the link below to set a new password:</p>
    <p><a href="${resetUrl}">${resetUrl}</a></p>
    <p>This link expires in 1 hour and can only be used once.</p>
    <p>If you did not request this, you can ignore this email.</p>
  `;
}

export const authRouter = createTRPCRouter({
  /**
   * Request a password reset email.
   * Always returns { success: true } — even if the email is not registered —
   * to prevent user enumeration.
   */
  requestPasswordReset: publicProcedure
    .input(z.object({ email: z.string().email() }))
    .mutation(async ({ ctx, input }) => {
      const ipKey = ctx.clientIp ? `ip:${ctx.clientIp}` : "";
      const keys = ipKey
        ? [`email:${input.email.toLowerCase()}`, ipKey]
        : [`email:${input.email.toLowerCase()}`];
      const rl = await authRateLimiter(keys);
      if (!rl.allowed) {
        throw new TRPCError({
          code: "TOO_MANY_REQUESTS",
          message: "Too many password reset attempts. Please wait before trying again.",
        });
      }

      const user = await ctx.db.user.findUnique({
        where: { email: input.email },
        select: { id: true, email: true },
      });

      if (!user) {
        // Timing-safe: don't reveal whether the email exists
        return { success: true };
      }

      // Delete any existing (unused) reset tokens for this email
      await ctx.db.passwordResetToken.deleteMany({
        where: { email: input.email, usedAt: null },
      });

      const token = randomBytes(32).toString("hex");
      const expiresAt = new Date(Date.now() + RESET_TTL_MS);

      await ctx.db.passwordResetToken.create({
        data: { email: input.email, token, expiresAt },
      });

      const resetUrl = `${getAppBaseUrl()}/auth/reset-password/${token}`;

      void sendEmail({
        to: input.email,
        subject: "CapaKraken — reset your password",
        text: `You requested a password reset.\n\nReset your password: ${resetUrl}\n\nThis link expires in 1 hour. If you did not request this, ignore this email.`,
        html: resetEmailHtml(resetUrl),
      });

      return { success: true };
    }),

  /** Validate a reset token and set a new password. */
  resetPassword: publicProcedure
    .input(
      z.object({
        token: z.string().min(1),
        password: z
          .string()
          .min(PASSWORD_MIN_LENGTH, PASSWORD_POLICY_MESSAGE)
          .max(PASSWORD_MAX_LENGTH),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      // Rate-limit keyed on IP (token is always new so token-keying is a no-op).
      // We cannot key on the resolved email before the token lookup; fall back
      // to IP-only here and apply an email-keyed limit AFTER the successful
      // lookup to bound per-email brute-force.
      const ipKey = ctx.clientIp ? `ip:${ctx.clientIp}` : "";
      if (ipKey) {
        const rl = await authRateLimiter(ipKey);
        if (!rl.allowed) {
          throw new TRPCError({
            code: "TOO_MANY_REQUESTS",
            message: "Too many password reset attempts. Please wait before trying again.",
          });
        }
      }

      const record = await ctx.db.passwordResetToken.findUnique({
        where: { token: input.token },
      });

      if (!record) {
        throw new TRPCError({ code: "NOT_FOUND", message: "Reset link not found." });
      }
      if (record.usedAt) {
        throw new TRPCError({
          code: "BAD_REQUEST",
          message: "This reset link has already been used.",
        });
      }
      if (record.expiresAt < new Date()) {
        throw new TRPCError({ code: "BAD_REQUEST", message: "This reset link has expired." });
      }

      // Second-layer limit keyed on the resolved email, so a targeted
      // attacker cannot exhaust reset attempts for a known user even if
      // they cycle source IPs.
      const emailRl = await authRateLimiter(`email-reset:${record.email.toLowerCase()}`);
      if (!emailRl.allowed) {
        throw new TRPCError({
          code: "TOO_MANY_REQUESTS",
          message: "Too many password reset attempts. Please wait before trying again.",
        });
      }

      // Reject weak/common/identity-related passwords *after* the token is
      // validated so attackers can't probe the policy without a valid link.
      const userForPolicy = await ctx.db.user.findUnique({
        where: { email: record.email },
        select: { email: true, name: true },
      });
      const policy = checkPasswordPolicy(input.password, userForPolicy ?? undefined);
      if (!policy.ok) {
        throw new TRPCError({ code: "BAD_REQUEST", message: policy.reason });
      }

      const { hash } = await import("@node-rs/argon2");
      const passwordHash = await hash(input.password);

      // All three operations must succeed atomically: if session deletion
      // fails after the password is already changed, old sessions could
      // persist with the new password (CWE-613).
      await ctx.db.$transaction(async (tx) => {
        const updatedUser = await tx.user.update({
          where: { email: record.email },
          data: { passwordHash },
          select: { id: true },
        });

        await tx.activeSession.deleteMany({ where: { userId: updatedUser.id } });

        await tx.passwordResetToken.update({
          where: { token: input.token },
          data: { usedAt: new Date() },
        });
      });

      return { success: true };
    }),
});
