import { randomBytes } from "node:crypto";
import { TRPCError } from "@trpc/server";
import { z } from "zod";
import { SystemRole } from "@capakraken/db";
import { createTRPCRouter, adminProcedure, publicProcedure } from "../trpc.js";
import { getAppBaseUrl } from "../lib/app-base-url.js";
import { sendEmail } from "../lib/email.js";

const INVITE_TTL_MS = 72 * 60 * 60 * 1000; // 72 hours

function inviteEmailHtml(inviteUrl: string, role: SystemRole): string {
  return `
    <p>You have been invited to join CapaKraken as <strong>${role}</strong>.</p>
    <p>Click the link below to accept the invitation and set your password:</p>
    <p><a href="${inviteUrl}">${inviteUrl}</a></p>
    <p>This link expires in 72 hours and can only be used once.</p>
  `;
}

export const inviteRouter = createTRPCRouter({
  /** Admin: create a new invite token and send the invite email. */
  createInvite: adminProcedure
    .input(
      z.object({
        email: z.string().email(),
        role: z.nativeEnum(SystemRole).default(SystemRole.USER),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      // Prevent duplicate pending invites for the same email
      const existing = await ctx.db.inviteToken.findFirst({
        where: { email: input.email, usedAt: null, expiresAt: { gt: new Date() } },
      });
      if (existing) {
        throw new TRPCError({
          code: "CONFLICT",
          message: "An active invite already exists for this email address.",
        });
      }

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

      await ctx.db.inviteToken.create({
        data: {
          email: input.email,
          role: input.role,
          token,
          expiresAt,
          createdById: ctx.dbUser!.id,
        },
      });

      const inviteUrl = `${getAppBaseUrl()}/invite/${token}`;

      void sendEmail({
        to: input.email,
        subject: "You have been invited to CapaKraken",
        text: `You have been invited to join CapaKraken as ${input.role}.\n\nAccept your invitation: ${inviteUrl}\n\nThis link expires in 72 hours.`,
        html: inviteEmailHtml(inviteUrl, input.role),
      });

      return { success: true };
    }),

  /** Admin: list all pending (unused, non-expired) invites. */
  listInvites: adminProcedure.query(async ({ ctx }) => {
    const invites = await ctx.db.inviteToken.findMany({
      where: { usedAt: null, expiresAt: { gt: new Date() } },
      orderBy: { createdAt: "desc" },
      select: { id: true, email: true, role: true, expiresAt: true, createdAt: true },
    });
    return invites;
  }),

  /** Admin: revoke a pending invite. */
  revokeInvite: adminProcedure
    .input(z.object({ id: z.string() }))
    .mutation(async ({ ctx, input }) => {
      await ctx.db.inviteToken.delete({ where: { id: input.id } });
      return { success: true };
    }),

  /** Public: look up invite token metadata (for the accept page). */
  getInvite: publicProcedure
    .input(z.object({ token: z.string() }))
    .query(async ({ ctx, input }) => {
      const invite = await ctx.db.inviteToken.findUnique({
        where: { token: input.token },
        select: { email: true, role: true, expiresAt: true, usedAt: true },
      });
      if (!invite) throw new TRPCError({ code: "NOT_FOUND", message: "Invite not found." });
      if (invite.usedAt) throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has already been used." });
      if (invite.expiresAt < new Date()) throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has expired." });
      return { email: invite.email, role: invite.role };
    }),

  /** Public: accept an invite — set password, create account. */
  acceptInvite: publicProcedure
    .input(
      z.object({
        token: z.string(),
        password: z.string().min(8, "Password must be at least 8 characters."),
      }),
    )
    .mutation(async ({ ctx, input }) => {
      const invite = await ctx.db.inviteToken.findUnique({
        where: { token: input.token },
      });
      if (!invite) throw new TRPCError({ code: "NOT_FOUND", message: "Invite not found." });
      if (invite.usedAt) throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has already been used." });
      if (invite.expiresAt < new Date()) throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has expired." });

      // Check if user already exists
      const existing = await ctx.db.user.findUnique({ where: { email: invite.email } });
      if (existing) {
        throw new TRPCError({ code: "CONFLICT", message: "An account with this email already exists." });
      }

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

      await ctx.db.user.create({
        data: {
          email: invite.email,
          name: invite.email.split("@")[0] ?? invite.email,
          systemRole: invite.role,
          passwordHash,
        },
      });

      await ctx.db.inviteToken.update({
        where: { token: input.token },
        data: { usedAt: new Date() },
      });

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