import { Router } from "express";
import { z } from "zod";
import type { Role } from "@/db/types";
import {
  User,
  RefreshToken,
  PasswordResetToken,
  EmailVerificationToken,
} from "@/db/models";
import { env } from "@/config/env";
import { comparePassword, hashPassword } from "@/lib/auth/password";
import { signAccessToken, signRefreshToken, verifyRefreshToken } from "@/lib/auth/jwt";
import { createRawToken, hashToken } from "@/lib/crypto";
import { AppError, asyncHandler } from "@/lib/errors";
import { requireAuth } from "@/middleware/auth";
import { sendMail } from "@/services/email";
import { issueEmailOtp, verifyEmailOtp } from "@/services/otp";
import { assertNotLocked, recordLoginFailure, clearLoginFailures } from "@/services/loginLock";
import { isValidE164Phone, normalizeEmail, normalizePhone } from "@/lib/phone";
import { isAuthMysql } from "@/db/activeDatabase";
import {
  createUserMysql,
  findActiveUserByEmailMysql,
  findUserByEmailMysql,
  findUserByIdMysql,
  findUserByPhoneMysql,
  updateUserMysql,
} from "@/db/mysql/users";
import {
  createEmailVerificationTokenMysql,
  createPasswordResetTokenMysql,
  createRefreshTokenMysql,
  findEmailVerificationByHashMysql,
  findPasswordResetByHashMysql,
  findRefreshTokenByHashMysql,
  markEmailVerificationUsedMysql,
  markPasswordResetUsedMysql,
  revokeAllRefreshTokensForUserMysql,
  revokeRefreshTokenByHashMysql,
  revokeRefreshTokenMysql,
} from "@/db/mysql/authTokens";

export const authRouter = Router();

function userNeedsOtp(user: { status?: string }) {
  return user.status === "PENDING_EMAIL_VERIFICATION";
}

const RegisterSchema = z.object({
  fullName: z.string().trim().min(2, "Full name is too short"),
  email: z
    .string()
    .trim()
    .transform(normalizeEmail)
    .pipe(z.string().email("Invalid email")),
  password: z.string().min(8, "Password must be at least 8 characters"),
  phone: z
    .string()
    .trim()
    .transform(normalizePhone)
    .refine(isValidE164Phone, "Invalid phone number"),
  role: z.enum(["CUSTOMER", "OWNER"]).default("CUSTOMER"),
  locale: z.enum(["ar", "en"]).optional(),
});

const LoginSchema = z.object({
  email: z
    .string()
    .trim()
    .transform(normalizeEmail)
    .pipe(z.string().email("Invalid email")),
  password: z.string().min(8),
});

function publicUser(user: {
  id?: string;
  _id?: string;
  email: string;
  fullName: string;
  role: Role | string;
  status: string;
  locale: string;
  phone?: string | null;
  avatarUrl?: string | null;
  emailVerifiedAt?: Date | null;
  trustedOwner?: boolean | null;
  ownerVerificationStatus?: string | null;
  passportUrl?: string | null;
  ownerVerifiedAt?: Date | null;
}) {
  return {
    id: String(user.id ?? user._id!),
    email: user.email,
    fullName: user.fullName,
    role: user.role,
    status: user.status,
    locale: user.locale,
    phone: user.phone ?? null,
    avatarUrl: user.avatarUrl ?? null,
    emailVerifiedAt: user.emailVerifiedAt ?? null,
    trustedOwner: Boolean(user.trustedOwner),
    ownerVerificationStatus: user.ownerVerificationStatus ?? "NONE",
    passportUrl: user.passportUrl ?? null,
    ownerVerifiedAt: user.ownerVerifiedAt ?? null,
  };
}

async function issueTokens(user: { id?: string; _id?: string; email: string; role: Role | string }) {
  const userId = String(user.id ?? user._id!);
  const jti = createRawToken(16);
  const refreshToken = signRefreshToken({ sub: userId, jti });
  const tokenHash = hashToken(refreshToken);
  const expiresAt = new Date(Date.now() + env.REFRESH_TOKEN_TTL_DAYS * 24 * 60 * 60 * 1000);

  if (isAuthMysql()) {
    await createRefreshTokenMysql({ userId, tokenHash, expiresAt });
  } else {
    await RefreshToken.create({ userId, tokenHash, expiresAt });
  }

  const accessToken = signAccessToken({
    sub: userId,
    email: user.email,
    role: user.role as Role,
  });
  return { accessToken, refreshToken, expiresAt };
}

async function sendVerificationEmail(userId: string, email: string, fullName: string) {
  const raw = createRawToken();
  const tokenHash = hashToken(raw);
  const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000);
  if (isAuthMysql()) {
    await createEmailVerificationTokenMysql({ userId, tokenHash, expiresAt });
  } else {
    await EmailVerificationToken.create({ userId, tokenHash, expiresAt });
  }
  const link = `${env.FRONTEND_URL}/verify-email?token=${raw}`;
  await sendMail({
    to: email,
    subject: `${env.APP_NAME} — Verify your email`,
    text: `Hello ${fullName},\n\nVerify your email: ${link}\n\nThis link expires in 24 hours.`,
    html: `<p>Hello ${fullName},</p><p><a href="${link}">Verify your email</a></p>`,
  });
}

authRouter.post(
  "/register",
  asyncHandler(async (req, res) => {
    const body = RegisterSchema.parse(req.body);
    const email = body.email;
    const phone = body.phone;

    if (isAuthMysql()) {
      if (await findUserByEmailMysql(email)) throw new AppError(409, "Email already registered");
      if (await findUserByPhoneMysql(phone)) throw new AppError(409, "Phone number already registered");

      const user = await createUserMysql({
        fullName: body.fullName,
        email,
        phone,
        passwordHash: await hashPassword(body.password),
        role: body.role,
        locale: body.locale ?? "ar",
        status: "PENDING_EMAIL_VERIFICATION",
      });

      await issueEmailOtp({
        userId: user.id,
        email: user.email,
        fullName: user.fullName,
      });

      const tokens = await issueTokens(user);
      res.status(201).json({
        user: publicUser(user),
        ...tokens,
        needsOtp: true,
        message: "Registered. Enter the 6-digit code sent to your email.",
        _db: "mysql",
      });
      return;
    }

    const existing = await User.findOne({ email }).lean();
    if (existing) throw new AppError(409, "Email already registered");

    const phoneTaken = await User.findOne({ phone }).lean();
    if (phoneTaken) throw new AppError(409, "Phone number already registered");

    const user = await User.create({
      fullName: body.fullName,
      email,
      phone,
      passwordHash: await hashPassword(body.password),
      role: body.role,
      locale: body.locale ?? "ar",
      status: "PENDING_EMAIL_VERIFICATION",
      emailVerifiedAt: undefined,
    });

    await issueEmailOtp({
      userId: user._id,
      email: user.email,
      fullName: user.fullName,
    });

    const tokens = await issueTokens(user);
    res.status(201).json({
      user: publicUser(user),
      ...tokens,
      needsOtp: true,
      message: "Registered. Enter the 6-digit code sent to your email.",
      _db: "mongodb",
    });
  }),
);

authRouter.post(
  "/login",
  asyncHandler(async (req, res) => {
    const body = LoginSchema.parse(req.body);
    const email = body.email;
    assertNotLocked(email);

    if (isAuthMysql()) {
      const user = await findActiveUserByEmailMysql(email);
      if (!user?.passwordHash) {
        recordLoginFailure(email);
        throw new AppError(401, "Invalid credentials");
      }
      const ok = await comparePassword(body.password, user.passwordHash);
      if (!ok) {
        const fail = recordLoginFailure(email);
        if (fail.locked) {
          throw new AppError(
            429,
            "Account temporarily locked after too many failed attempts. Try again in 15 minutes.",
          );
        }
        throw new AppError(401, "Invalid credentials");
      }
      clearLoginFailures(email);
      if (user.status === "SUSPENDED") throw new AppError(403, "Account suspended");

      const needsOtp = userNeedsOtp(user);
      if (needsOtp) {
        try {
          await issueEmailOtp({
            userId: user.id,
            email: user.email,
            fullName: user.fullName,
          });
        } catch (err) {
          if (!(err instanceof AppError && err.status === 429)) throw err;
        }
      }

      const tokens = await issueTokens(user);
      res.json({ user: publicUser(user), ...tokens, needsOtp, _db: "mysql" });
      return;
    }

    const user = await User.findOne({ email, deletedAt: null });
    if (!user?.passwordHash) {
      recordLoginFailure(email);
      throw new AppError(401, "Invalid credentials");
    }
    const ok = await comparePassword(body.password, user.passwordHash);
    if (!ok) {
      const fail = recordLoginFailure(email);
      if (fail.locked) {
        throw new AppError(
          429,
          "Account temporarily locked after too many failed attempts. Try again in 15 minutes.",
        );
      }
      throw new AppError(401, "Invalid credentials");
    }
    clearLoginFailures(email);
    if (user.status === "SUSPENDED") throw new AppError(403, "Account suspended");

    const needsOtp = userNeedsOtp(user);
    if (needsOtp) {
      try {
        await issueEmailOtp({
          userId: user._id,
          email: user.email,
          fullName: user.fullName,
        });
      } catch (err) {
        if (!(err instanceof AppError && err.status === 429)) throw err;
      }
    }

    const tokens = await issueTokens(user);
    res.json({ user: publicUser(user), ...tokens, needsOtp, _db: "mongodb" });
  }),
);

authRouter.post(
  "/verify-otp",
  requireAuth,
  asyncHandler(async (req, res) => {
    const code = z.string().min(4).max(8).parse(req.body.code);

    if (isAuthMysql()) {
      const user = await findUserByIdMysql(req.user!.id);
      if (!user) throw new AppError(404, "User not found");
      if (user.emailVerifiedAt && user.status === "ACTIVE") {
        return res.json({
          user: publicUser(user),
          needsOtp: false,
          message: "Already verified",
          _db: "mysql",
        });
      }
      await verifyEmailOtp({ userId: user.id, code });
      const updated = await updateUserMysql(user.id, {
        status: "ACTIVE",
        emailVerifiedAt: new Date(),
      });
      res.json({
        user: publicUser(updated!),
        needsOtp: false,
        message: "Email verified",
        _db: "mysql",
      });
      return;
    }

    const user = await User.findById(req.user!.id);
    if (!user) throw new AppError(404, "User not found");
    if (user.emailVerifiedAt && user.status === "ACTIVE") {
      return res.json({
        user: publicUser(user),
        needsOtp: false,
        message: "Already verified",
        _db: "mongodb",
      });
    }

    await verifyEmailOtp({ userId: user._id, code });
    user.status = "ACTIVE";
    user.emailVerifiedAt = new Date();
    await user.save();

    res.json({
      user: publicUser(user),
      needsOtp: false,
      message: "Email verified",
      _db: "mongodb",
    });
  }),
);

authRouter.post(
  "/resend-otp",
  requireAuth,
  asyncHandler(async (req, res) => {
    if (isAuthMysql()) {
      const user = await findUserByIdMysql(req.user!.id);
      if (!user) throw new AppError(404, "User not found");
      if (user.emailVerifiedAt && user.status === "ACTIVE") {
        throw new AppError(400, "Email already verified");
      }
      const result = await issueEmailOtp({
        userId: user.id,
        email: user.email,
        fullName: user.fullName,
      });
      res.json({
        message: "OTP resent to your email",
        expiresAt: result.expiresAt,
        _db: "mysql",
      });
      return;
    }

    const user = await User.findById(req.user!.id);
    if (!user) throw new AppError(404, "User not found");
    if (user.emailVerifiedAt && user.status === "ACTIVE") {
      throw new AppError(400, "Email already verified");
    }

    const result = await issueEmailOtp({
      userId: user._id,
      email: user.email,
      fullName: user.fullName,
    });

    res.json({
      message: "OTP resent to your email",
      expiresAt: result.expiresAt,
      _db: "mongodb",
    });
  }),
);

authRouter.post(
  "/refresh",
  asyncHandler(async (req, res) => {
    const refreshToken = z.string().min(10).parse(req.body.refreshToken);
    let payload;
    try {
      payload = verifyRefreshToken(refreshToken);
    } catch {
      throw new AppError(401, "Invalid refresh token");
    }
    const tokenHash = hashToken(refreshToken);

    if (isAuthMysql()) {
      const stored = await findRefreshTokenByHashMysql(tokenHash);
      if (!stored || stored.revokedAt || stored.expiresAt < new Date()) {
        throw new AppError(401, "Refresh token revoked or expired");
      }
      const user = await findUserByIdMysql(payload.sub);
      if (!user || user.deletedAt || user.status === "SUSPENDED") {
        throw new AppError(401, "Unauthorized");
      }
      await revokeRefreshTokenMysql(stored.id);
      const tokens = await issueTokens(user);
      res.json({ user: publicUser(user), ...tokens, _db: "mysql" });
      return;
    }

    const stored = await RefreshToken.findOne({ tokenHash });
    if (!stored || stored.revokedAt || stored.expiresAt < new Date()) {
      throw new AppError(401, "Refresh token revoked or expired");
    }
    const user = await User.findOne({ _id: payload.sub, deletedAt: null });
    if (!user || user.status === "SUSPENDED") throw new AppError(401, "Unauthorized");

    stored.revokedAt = new Date();
    await stored.save();
    const tokens = await issueTokens(user);
    res.json({ user: publicUser(user), ...tokens, _db: "mongodb" });
  }),
);

authRouter.post(
  "/logout",
  asyncHandler(async (req, res) => {
    const refreshToken = z.string().optional().parse(req.body.refreshToken);
    if (refreshToken) {
      const tokenHash = hashToken(refreshToken);
      if (isAuthMysql()) {
        await revokeRefreshTokenByHashMysql(tokenHash);
      } else {
        await RefreshToken.updateMany(
          { tokenHash, revokedAt: null },
          { $set: { revokedAt: new Date() } },
        );
      }
    }
    res.json({ ok: true, _db: isAuthMysql() ? "mysql" : "mongodb" });
  }),
);

authRouter.get(
  "/me",
  requireAuth,
  asyncHandler(async (req, res) => {
    if (isAuthMysql()) {
      const user = await findUserByIdMysql(req.user!.id);
      if (!user) throw new AppError(404, "User not found");
      res.json({ user: publicUser(user), _db: "mysql" });
      return;
    }
    const user = await User.findById(req.user!.id);
    if (!user) throw new AppError(404, "User not found");
    res.json({ user: publicUser(user), _db: "mongodb" });
  }),
);

authRouter.patch(
  "/profile",
  requireAuth,
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        fullName: z.string().min(2).optional(),
        phone: z.string().min(6).max(30).optional().nullable(),
        locale: z.enum(["ar", "en"]).optional(),
        avatarUrl: z.string().url().optional().nullable(),
      })
      .parse(req.body);

    if (isAuthMysql()) {
      const user = await updateUserMysql(req.user!.id, body);
      if (!user) throw new AppError(404, "User not found");
      res.json({ user: publicUser(user), _db: "mysql" });
      return;
    }

    const user = await User.findByIdAndUpdate(req.user!.id, { $set: body }, { new: true });
    if (!user) throw new AppError(404, "User not found");
    res.json({ user: publicUser(user), _db: "mongodb" });
  }),
);

authRouter.post(
  "/forgot-password",
  asyncHandler(async (req, res) => {
    const email = z.string().email().parse(req.body.email).toLowerCase();

    if (isAuthMysql()) {
      const user = await findActiveUserByEmailMysql(email);
      if (user) {
        const raw = createRawToken();
        await createPasswordResetTokenMysql({
          userId: user.id,
          tokenHash: hashToken(raw),
          expiresAt: new Date(Date.now() + 60 * 60 * 1000),
        });
        const link = `${env.FRONTEND_URL}/reset-password?token=${raw}`;
        await sendMail({
          to: user.email,
          subject: `${env.APP_NAME} — Reset password`,
          text: `Reset your password: ${link}\n\nExpires in 1 hour.`,
        });
      }
      res.json({
        message: "If the email exists, a reset link was sent.",
        _db: "mysql",
      });
      return;
    }

    const user = await User.findOne({ email, deletedAt: null });
    if (user) {
      const raw = createRawToken();
      await PasswordResetToken.create({
        userId: user._id,
        tokenHash: hashToken(raw),
        expiresAt: new Date(Date.now() + 60 * 60 * 1000),
      });
      const link = `${env.FRONTEND_URL}/reset-password?token=${raw}`;
      await sendMail({
        to: user.email,
        subject: `${env.APP_NAME} — Reset password`,
        text: `Reset your password: ${link}\n\nExpires in 1 hour.`,
      });
    }
    res.json({
      message: "If the email exists, a reset link was sent.",
      _db: "mongodb",
    });
  }),
);

authRouter.post(
  "/reset-password",
  asyncHandler(async (req, res) => {
    const body = z
      .object({
        token: z.string().min(10),
        password: z.string().min(8),
      })
      .parse(req.body);

    const tokenHash = hashToken(body.token);

    if (isAuthMysql()) {
      const record = await findPasswordResetByHashMysql(tokenHash);
      if (!record || record.usedAt || record.expiresAt < new Date()) {
        throw new AppError(400, "Invalid or expired reset token");
      }
      await updateUserMysql(record.userId, {
        passwordHash: await hashPassword(body.password),
      });
      await markPasswordResetUsedMysql(record.id);
      await revokeAllRefreshTokensForUserMysql(record.userId);
      res.json({ message: "Password updated", _db: "mysql" });
      return;
    }

    const record = await PasswordResetToken.findOne({ tokenHash });
    if (!record || record.usedAt || record.expiresAt < new Date()) {
      throw new AppError(400, "Invalid or expired reset token");
    }

    await User.updateOne(
      { _id: record.userId },
      { $set: { passwordHash: await hashPassword(body.password) } },
    );
    record.usedAt = new Date();
    await record.save();
    await RefreshToken.updateMany(
      { userId: record.userId, revokedAt: null },
      { $set: { revokedAt: new Date() } },
    );

    res.json({ message: "Password updated", _db: "mongodb" });
  }),
);

authRouter.post(
  "/verify-email",
  asyncHandler(async (req, res) => {
    const token = z.string().min(10).parse(req.body.token);
    const tokenHash = hashToken(token);

    if (isAuthMysql()) {
      const record = await findEmailVerificationByHashMysql(tokenHash);
      if (!record || record.usedAt || record.expiresAt < new Date()) {
        throw new AppError(400, "Invalid or expired verification token");
      }
      await updateUserMysql(record.userId, {
        status: "ACTIVE",
        emailVerifiedAt: new Date(),
      });
      await markEmailVerificationUsedMysql(record.id);
      res.json({ message: "Email verified", _db: "mysql" });
      return;
    }

    const record = await EmailVerificationToken.findOne({ tokenHash });
    if (!record || record.usedAt || record.expiresAt < new Date()) {
      throw new AppError(400, "Invalid or expired verification token");
    }

    await User.updateOne(
      { _id: record.userId },
      { $set: { status: "ACTIVE", emailVerifiedAt: new Date() } },
    );
    record.usedAt = new Date();
    await record.save();

    res.json({ message: "Email verified", _db: "mongodb" });
  }),
);

authRouter.post(
  "/resend-verification",
  requireAuth,
  asyncHandler(async (req, res) => {
    if (isAuthMysql()) {
      const user = await findUserByIdMysql(req.user!.id);
      if (!user) throw new AppError(404, "User not found");
      if (user.emailVerifiedAt) throw new AppError(400, "Email already verified");
      await sendVerificationEmail(user.id, user.email, user.fullName);
      res.json({ message: "Verification email sent", _db: "mysql" });
      return;
    }
    const user = await User.findById(req.user!.id);
    if (!user) throw new AppError(404, "User not found");
    if (user.emailVerifiedAt) throw new AppError(400, "Email already verified");
    await sendVerificationEmail(user._id, user.email, user.fullName);
    res.json({ message: "Verification email sent", _db: "mongodb" });
  }),
);
