import bcrypt from "bcryptjs";
import { Router } from "express";
import jwt from "jsonwebtoken";
import { z } from "zod";
import { env, systemAdminEmails } from "../lib/env";
import { colorForSeed } from "../lib/avatar";
import { acceptPendingInvitationsForEmail } from "../lib/invitations";
import { signAuthToken } from "../lib/jwt";
import { emailTemplate, sendEmail } from "../lib/mailer";
import { prisma } from "../lib/prisma";
import { HttpError } from "../middleware/errorHandler";
import { authLimiter } from "../middleware/rateLimit";
import { AUTH_COOKIE_NAME, requireAuth } from "../middleware/requireAuth";
import { uniqueOrganizationSlug } from "../lib/slugs";

export const authRouter = Router();

const COOKIE_OPTIONS = {
  httpOnly: true,
  sameSite: "lax" as const,
  secure: env.NODE_ENV === "production",
  maxAge: 7 * 24 * 60 * 60 * 1000,
};

function publicUser(user: {
  id: string;
  email: string;
  name: string;
  username: string;
  title: string | null;
  avatarUrl: string | null;
  avatarColor: string;
  emailVerified: boolean;
  isSystemAdmin: boolean;
}) {
  return {
    id: user.id,
    email: user.email,
    name: user.name,
    username: user.username,
    title: user.title,
    avatarUrl: user.avatarUrl,
    avatarColor: user.avatarColor,
    emailVerified: user.emailVerified,
    isSystemAdmin: user.isSystemAdmin,
  };
}

/** Grants isSystemAdmin to accounts listed in SYSTEM_ADMIN_EMAILS on login/registration,
 * so the platform owner can bootstrap the first system admin via server config rather
 * than a direct database write. */
async function reconcileSystemAdmin<T extends { id: string; email: string; isSystemAdmin: boolean }>(
  user: T
): Promise<T> {
  if (user.isSystemAdmin || !systemAdminEmails.has(user.email.toLowerCase())) return user;
  const updated = await prisma.user.update({ where: { id: user.id }, data: { isSystemAdmin: true } });
  return { ...user, isSystemAdmin: updated.isSystemAdmin };
}

async function sendVerificationEmail(user: { id: string; email: string; name: string }) {
  const token = jwt.sign({ userId: user.id, purpose: "verify-email" }, env.JWT_SECRET, {
    expiresIn: "24h",
  });
  const verifyUrl = `${env.CLIENT_URL}/verify-email?token=${token}`;

  await sendEmail({
    to: user.email,
    subject: "Verify your email for My Assisto",
    html: emailTemplate({
      title: "Confirm your email address",
      bodyHtml: `Hi ${user.name}, welcome to My Assisto! Please confirm this is your email address. This link expires in 24 hours.`,
      ctaLabel: "Verify email",
      ctaUrl: verifyUrl,
    }),
  });
}

const registerSchema = z.object({
  name: z.string().min(1).max(100),
  username: z
    .string()
    .min(3)
    .max(30)
    .regex(/^[a-zA-Z0-9_.]+$/, "Username can only contain letters, numbers, dots and underscores"),
  email: z.string().email(),
  password: z.string().min(8).max(200),
  organizationName: z.string().min(1).max(100),
  inviteToken: z.string().optional(),
});

authRouter.post("/register", authLimiter, async (req, res, next) => {
  try {
    const data = registerSchema.parse(req.body);

    const existing = await prisma.user.findFirst({
      where: { OR: [{ email: data.email }, { username: data.username }] },
    });
    if (existing) {
      throw new HttpError(409, "A user with this email or username already exists");
    }

    const passwordHash = await bcrypt.hash(data.password, 12);
    const organizationSlug = await uniqueOrganizationSlug(data.organizationName);

    // Every new registrant gets their own organization (as its admin), regardless
    // of whether they're also joining someone else's via an invite below — those
    // are additive, not a replacement for having your own organization. Workspaces
    // (departments) within it are created afterward, by name, from the org page.
    const user = await prisma.user.create({
      data: {
        name: data.name,
        username: data.username,
        email: data.email,
        passwordHash,
        avatarColor: colorForSeed(data.email),
        notificationPreferences: { create: {} },
        organizationMemberships: {
          create: {
            role: "OWNER",
            organization: { create: { name: data.organizationName, slug: organizationSlug } },
          },
        },
      },
    });

    if (data.inviteToken) {
      const invitation = await prisma.invitation.findUnique({ where: { token: data.inviteToken } });
      if (invitation && !invitation.acceptedAt && invitation.email === data.email) {
        await acceptPendingInvitationsForEmail(user.id, data.email);
      }
    }

    const reconciled = await reconcileSystemAdmin(user);
    const token = signAuthToken({ userId: user.id });
    res.cookie(AUTH_COOKIE_NAME, token, COOKIE_OPTIONS);
    res.status(201).json({ user: publicUser(reconciled) });

    sendVerificationEmail(user).catch((err) => console.error("[verify-email]", err));
  } catch (err) {
    next(err);
  }
});

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string().min(1),
});

authRouter.post("/login", authLimiter, async (req, res, next) => {
  try {
    const data = loginSchema.parse(req.body);

    const user = await prisma.user.findUnique({
      where: { email: data.email },
      omit: { passwordHash: false },
    });
    if (!user) {
      throw new HttpError(401, "Invalid email or password");
    }

    const valid = await bcrypt.compare(data.password, user.passwordHash);
    if (!valid) {
      throw new HttpError(401, "Invalid email or password");
    }

    const reconciled = await reconcileSystemAdmin(user);
    const token = signAuthToken({ userId: user.id });
    res.cookie(AUTH_COOKIE_NAME, token, COOKIE_OPTIONS);
    res.json({ user: publicUser(reconciled) });
  } catch (err) {
    next(err);
  }
});

authRouter.post("/logout", (_req, res) => {
  res.clearCookie(AUTH_COOKIE_NAME, { httpOnly: true, sameSite: "lax", secure: env.NODE_ENV === "production" });
  res.status(204).send();
});

authRouter.get("/me", requireAuth, async (req, res, next) => {
  try {
    const user = await prisma.user.findUnique({ where: { id: req.userId } });
    if (!user) {
      throw new HttpError(401, "Not authenticated");
    }
    const reconciled = await reconcileSystemAdmin(user);
    res.json({ user: publicUser(reconciled) });
  } catch (err) {
    next(err);
  }
});

const forgotPasswordSchema = z.object({ email: z.string().email() });

authRouter.post("/forgot-password", authLimiter, async (req, res, next) => {
  try {
    const { email } = forgotPasswordSchema.parse(req.body);
    const user = await prisma.user.findUnique({ where: { email } });

    // Always respond 200 regardless of whether the account exists, to avoid leaking which emails are registered.
    if (user) {
      const resetToken = jwt.sign({ userId: user.id, purpose: "reset" }, env.JWT_SECRET, {
        expiresIn: "1h",
      });
      const resetUrl = `${env.CLIENT_URL}/reset-password?token=${resetToken}`;

      await sendEmail({
        to: user.email,
        subject: "Reset your My Assisto password",
        html: emailTemplate({
          title: "Reset your password",
          bodyHtml: `Hi ${user.name}, click the button below to choose a new password. This link expires in 1 hour.`,
          ctaLabel: "Reset password",
          ctaUrl: resetUrl,
        }),
      });
    }

    res.status(200).json({ message: "If that email exists, a reset link has been sent." });
  } catch (err) {
    next(err);
  }
});

const resetPasswordSchema = z.object({
  token: z.string().min(1),
  password: z.string().min(8).max(200),
});

authRouter.post("/reset-password", authLimiter, async (req, res, next) => {
  try {
    const data = resetPasswordSchema.parse(req.body);

    let payload: { userId: string; purpose: string };
    try {
      payload = jwt.verify(data.token, env.JWT_SECRET) as typeof payload;
    } catch {
      throw new HttpError(400, "This reset link is invalid or has expired");
    }

    if (payload.purpose !== "reset") {
      throw new HttpError(400, "This reset link is invalid or has expired");
    }

    const passwordHash = await bcrypt.hash(data.password, 12);
    await prisma.user.update({
      where: { id: payload.userId },
      data: { passwordHash },
    });

    res.status(200).json({ message: "Password updated. You can now sign in." });
  } catch (err) {
    next(err);
  }
});

const verifyEmailSchema = z.object({ token: z.string().min(1) });

authRouter.post("/verify-email", authLimiter, async (req, res, next) => {
  try {
    const { token } = verifyEmailSchema.parse(req.body);

    let payload: { userId: string; purpose: string };
    try {
      payload = jwt.verify(token, env.JWT_SECRET) as typeof payload;
    } catch {
      throw new HttpError(400, "This verification link is invalid or has expired");
    }

    if (payload.purpose !== "verify-email") {
      throw new HttpError(400, "This verification link is invalid or has expired");
    }

    const user = await prisma.user.update({
      where: { id: payload.userId },
      data: { emailVerified: true },
    });

    res.json({ user: publicUser(user) });
  } catch (err) {
    next(err);
  }
});

authRouter.post("/resend-verification", authLimiter, requireAuth, async (req, res, next) => {
  try {
    const user = await prisma.user.findUnique({ where: { id: req.userId } });
    if (!user) throw new HttpError(401, "Not authenticated");
    if (user.emailVerified) {
      res.status(200).json({ message: "Your email is already verified." });
      return;
    }

    await sendVerificationEmail(user);
    res.status(200).json({ message: "Verification email sent." });
  } catch (err) {
    next(err);
  }
});
