import { Router, type IRouter } from "express";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import { randomBytes, createHash } from "node:crypto";
import { db, usersTable, membershipsTable, passwordResetTokensTable } from "@workspace/db";
import { eq, or, and, isNull, gt } from "drizzle-orm";
import {
  RegisterBody,
  LoginBody,
  RegisterModelBody,
  ForgotPasswordBody,
  ResetPasswordBody,
} from "@workspace/api-zod";
import { JWT_SECRET, requireAuth } from "../middlewares/auth";
import { insertProfileWithGeneratedId, serializeProfile } from "../lib/profile";
import { sendPasswordResetEmail } from "../lib/email";
import { logger } from "../lib/logger";

const router: IRouter = Router();

function serializeUser(user: typeof usersTable.$inferSelect) {
  return {
    id: user.id,
    email: user.email,
    name: user.name,
    phone: user.phone,
    role: user.role,
    isOnline: user.isOnline,
    avatarUrl: user.avatarUrl,
    bio: user.bio,
    createdAt: user.createdAt,
  };
}

router.post("/auth/register", async (req, res): Promise<void> => {
  const parsed = RegisterBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }
  const { email, password, name, phone } = parsed.data;

  const [existing] = await db.select().from(usersTable).where(eq(usersTable.email, email));
  if (existing) {
    res.status(400).json({ error: "Email already registered" });
    return;
  }

  if (phone) {
    const [phoneTaken] = await db.select().from(usersTable).where(eq(usersTable.phone, phone));
    if (phoneTaken) {
      res.status(400).json({ error: "Mobile number already registered" });
      return;
    }
  }

  const passwordHash = await bcrypt.hash(password, 10);
  const [user] = await db.insert(usersTable).values({
    email,
    passwordHash,
    name,
    phone: phone ?? null,
    role: "user",
    isOnline: true,
  }).returning();

  await db.insert(membershipsTable).values({ userId: user.id, tier: "free" });

  const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: "30d" });
  res.status(201).json({ user: serializeUser(user), token });
});

router.post("/auth/register-model", async (req, res): Promise<void> => {
  const parsed = RegisterModelBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }
  const d = parsed.data;

  const [existing] = await db.select().from(usersTable).where(eq(usersTable.email, d.email));
  if (existing) {
    res.status(400).json({ error: "Email already registered" });
    return;
  }

  const passwordHash = await bcrypt.hash(d.password, 10);
  const [user] = await db.insert(usersTable).values({
    email: d.email,
    passwordHash,
    name: d.name,
    role: "model",
    isOnline: true,
  }).returning();

  await db.insert(membershipsTable).values({ userId: user.id, tier: "free" });

  const profile = await insertProfileWithGeneratedId({
    userId: user.id,
    name: d.name,
    city: d.city,
    proStatus: d.proStatus,
    price1h: d.price1h,
    price2h: d.price2h,
    priceFullDay: d.priceFullDay,
    services: d.services,
    photoUrl: d.photoUrl ?? undefined,
    bio: d.bio ?? undefined,
    contactPlatform: "whatsapp",
    whatsappNumber: d.whatsappNumber,
    approvalStatus: "pending",
  });

  const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: "30d" });
  res.status(201).json({ user: serializeUser(user), token, profile: serializeProfile(profile) });
});

router.post("/auth/login", async (req, res): Promise<void> => {
  const parsed = LoginBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }
  const { email, password } = parsed.data;
  const identifier = email.trim();

  const [user] = await db
    .select()
    .from(usersTable)
    .where(or(eq(usersTable.email, identifier), eq(usersTable.phone, identifier)));
  if (!user) {
    res.status(401).json({ error: "Invalid credentials" });
    return;
  }
  const valid = await bcrypt.compare(password, user.passwordHash);
  if (!valid) {
    res.status(401).json({ error: "Invalid credentials" });
    return;
  }

  await db.update(usersTable).set({ isOnline: true }).where(eq(usersTable.id, user.id));

  const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: "30d" });
  res.json({ user: { ...serializeUser(user), isOnline: true }, token });
});

router.post("/auth/logout", requireAuth, async (req, res): Promise<void> => {
  if (req.user) {
    await db.update(usersTable).set({ isOnline: false }).where(eq(usersTable.id, req.user.id));
  }
  res.sendStatus(204);
});

router.post("/auth/forgot-password", async (req, res): Promise<void> => {
  const parsed = ForgotPasswordBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }
  const email = parsed.data.email.trim();

  const [user] = await db.select().from(usersTable).where(eq(usersTable.email, email));
  // Always respond 204 to avoid leaking which emails are registered.
  if (user) {
    const rawToken = randomBytes(32).toString("hex");
    const tokenHash = createHash("sha256").update(rawToken).digest("hex");
    const expiresAt = new Date(Date.now() + 60 * 60 * 1000); // 1 hour
    await db.insert(passwordResetTokensTable).values({ userId: user.id, tokenHash, expiresAt });

    const appUrl = process.env.APP_PUBLIC_URL ?? `https://${process.env.REPLIT_DEV_DOMAIN ?? ""}`;
    const resetUrl = `${appUrl}/reset-password?token=${rawToken}`;
    try {
      await sendPasswordResetEmail(user.email, resetUrl);
    } catch (err) {
      logger.error({ err }, "[auth] failed to send password reset email");
    }
  }
  res.sendStatus(204);
});

router.post("/auth/reset-password", async (req, res): Promise<void> => {
  const parsed = ResetPasswordBody.safeParse(req.body);
  if (!parsed.success) {
    res.status(400).json({ error: parsed.error.message });
    return;
  }
  const { token, password } = parsed.data;
  const tokenHash = createHash("sha256").update(token).digest("hex");

  const passwordHash = await bcrypt.hash(password, 10);

  // Atomically consume the token: only one concurrent request can flip usedAt
  // from NULL while it is unexpired. The returned row proves we won the race.
  const consumed = await db
    .update(passwordResetTokensTable)
    .set({ usedAt: new Date() })
    .where(
      and(
        eq(passwordResetTokensTable.tokenHash, tokenHash),
        isNull(passwordResetTokensTable.usedAt),
        gt(passwordResetTokensTable.expiresAt, new Date()),
      ),
    )
    .returning();

  const [row] = consumed;
  if (!row) {
    res.status(400).json({ error: "Invalid or expired token" });
    return;
  }

  await db.update(usersTable).set({ passwordHash }).where(eq(usersTable.id, row.userId));

  res.sendStatus(204);
});

router.get("/auth/me", requireAuth, async (req, res): Promise<void> => {
  const [user] = await db.select().from(usersTable).where(eq(usersTable.id, req.user!.id));
  if (!user) {
    res.status(404).json({ error: "User not found" });
    return;
  }
  res.json(serializeUser(user));
});

router.patch("/auth/me/update", requireAuth, async (req, res): Promise<void> => {
  const { name, bio, avatarUrl, phone } = req.body;
  const updateData: Record<string, string> = {};
  if (name) updateData.name = name;
  if (bio !== undefined) updateData.bio = bio;
  if (avatarUrl !== undefined) updateData.avatarUrl = avatarUrl;
  if (phone !== undefined) updateData.phone = phone;

  const [user] = await db.update(usersTable).set(updateData).where(eq(usersTable.id, req.user!.id)).returning();
  res.json(serializeUser(user));
});

export default router;
