"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getCurrentUser = exports.updateCustomerProfile = exports.changePassword = exports.logoutAll = exports.logout = exports.refreshAuthToken = exports.verifyStaffOtp = exports.requestStaffOtp = exports.loginStaff = exports.registerStaff = exports.verifyCustomerOtp = exports.requestCustomerOtp = void 0;
const bcryptjs_1 = __importDefault(require("bcryptjs"));
const drizzle_orm_1 = require("drizzle-orm");
const index_js_1 = require("../db/index.js");
const schema_js_1 = require("../db/schema.js");
const authSerializers_js_1 = require("../services/authSerializers.js");
const authSecurity_js_1 = require("../services/authSecurity.js");
const mailer_js_1 = require("../services/mailer.js");
const sessionService_js_1 = require("../services/sessionService.js");
const apiResponse_js_1 = require("../utils/apiResponse.js");
const MAX_OTP_ATTEMPTS = 5;
const getRequestIp = (req) => req.ip || req.socket.remoteAddress || undefined;
const getRequestUserAgent = (req) => req.headers["user-agent"]?.slice(0, 512);
const createAndSendOtp = async ({ actorType, email, purpose, }) => {
    const code = (0, authSecurity_js_1.generateOtpCode)();
    await index_js_1.db.insert(schema_js_1.authOtps).values({
        actorType,
        email,
        codeHash: (0, authSecurity_js_1.hashOtpCode)(code),
        purpose,
        expiresAt: (0, authSecurity_js_1.otpExpiryDate)(),
    });
    const delivery = await (0, mailer_js_1.sendOtpEmail)({
        to: email,
        code,
        purpose: actorType === "customer" ? "Customer login" : "Staff verification",
        expiresInMinutes: authSecurity_js_1.OTP_EXPIRES_IN_MINUTES,
    });
    return { delivery, code };
};
const consumeValidOtp = async ({ actorType, email, code, purpose, }) => {
    const [otp] = await index_js_1.db
        .select()
        .from(schema_js_1.authOtps)
        .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(schema_js_1.authOtps.actorType, actorType), (0, drizzle_orm_1.eq)(schema_js_1.authOtps.email, email), (0, drizzle_orm_1.eq)(schema_js_1.authOtps.purpose, purpose), (0, drizzle_orm_1.isNull)(schema_js_1.authOtps.consumedAt), (0, drizzle_orm_1.gt)(schema_js_1.authOtps.expiresAt, new Date())))
        .orderBy((0, drizzle_orm_1.desc)(schema_js_1.authOtps.createdAt))
        .limit(1);
    if (!otp)
        return false;
    if (otp.attempts >= MAX_OTP_ATTEMPTS) {
        await index_js_1.db
            .update(schema_js_1.authOtps)
            .set({ consumedAt: new Date() })
            .where((0, drizzle_orm_1.eq)(schema_js_1.authOtps.id, otp.id));
        return false;
    }
    if (otp.codeHash !== (0, authSecurity_js_1.hashOtpCode)(code)) {
        await index_js_1.db
            .update(schema_js_1.authOtps)
            .set({
            attempts: otp.attempts + 1,
            ...(otp.attempts + 1 >= MAX_OTP_ATTEMPTS
                ? { consumedAt: new Date() }
                : {}),
        })
            .where((0, drizzle_orm_1.eq)(schema_js_1.authOtps.id, otp.id));
        return false;
    }
    await index_js_1.db
        .update(schema_js_1.authOtps)
        .set({ consumedAt: new Date() })
        .where((0, drizzle_orm_1.eq)(schema_js_1.authOtps.id, otp.id));
    return true;
};
const sendOtpRequestResponse = (res, email, delivery, code) => {
    return (0, apiResponse_js_1.sendSuccess)(res, 200, {
        email,
        expiresInMinutes: authSecurity_js_1.OTP_EXPIRES_IN_MINUTES,
        delivery,
        ...(code ? { code } : {}),
    }, delivery === "console"
        ? "OTP generated. Check the server console in development."
        : "OTP sent to email.");
};
const requestCustomerOtp = async (req, res) => {
    const body = req.validatedBody;
    const { delivery, code } = await createAndSendOtp({
        actorType: "customer",
        email: body.email,
        purpose: "login",
    });
    return sendOtpRequestResponse(res, body.email, delivery, code);
};
exports.requestCustomerOtp = requestCustomerOtp;
const verifyCustomerOtp = async (req, res) => {
    const body = req.validatedBody;
    const isValid = await consumeValidOtp({
        actorType: "customer",
        email: body.email,
        code: body.code,
        purpose: "login",
    });
    if (!isValid) {
        return (0, apiResponse_js_1.sendError)(res, 401, "INVALID_OTP", "Invalid or expired OTP.");
    }
    const [existingCustomer] = await index_js_1.db
        .select()
        .from(schema_js_1.customers)
        .where((0, drizzle_orm_1.eq)(schema_js_1.customers.email, body.email))
        .limit(1);
    const now = new Date();
    if (existingCustomer) {
        await index_js_1.db
            .update(schema_js_1.customers)
            .set({
            ...(body.name ? { name: body.name } : {}),
            ...(body.phoneNumber ? { phoneNumber: body.phoneNumber } : {}),
            emailVerifiedAt: existingCustomer.emailVerifiedAt ?? now,
            lastLoginAt: now,
        })
            .where((0, drizzle_orm_1.eq)(schema_js_1.customers.id, existingCustomer.id));
    }
    else {
        await index_js_1.db.insert(schema_js_1.customers).values({
            email: body.email,
            name: body.name,
            phoneNumber: body.phoneNumber,
            emailVerifiedAt: now,
            lastLoginAt: now,
        });
    }
    const [customer] = await index_js_1.db
        .select()
        .from(schema_js_1.customers)
        .where((0, drizzle_orm_1.eq)(schema_js_1.customers.email, body.email))
        .limit(1);
    if (!customer)
        return (0, apiResponse_js_1.sendError)(res, 500, "CUSTOMER_AUTH_FAILED", "Could not authenticate customer.");
    const publicCustomer = (0, authSerializers_js_1.toPublicCustomer)(customer);
    const session = await (0, sessionService_js_1.createAuthSession)({
        actorType: "customer",
        actorId: publicCustomer.id,
        userAgent: getRequestUserAgent(req),
        ipAddress: getRequestIp(req),
    });
    return (0, apiResponse_js_1.sendSuccess)(res, 200, { ...session, user: publicCustomer }, "Customer authenticated.");
};
exports.verifyCustomerOtp = verifyCustomerOtp;
const registerStaff = async (req, res) => {
    const body = req.validatedBody;
    const [firstStaff] = await index_js_1.db
        .select({ id: schema_js_1.staffUsers.id })
        .from(schema_js_1.staffUsers)
        .limit(1);
    const isBootstrapRegistration = !firstStaff;
    if (!isBootstrapRegistration) {
        if (req.auth?.actorType !== "staff" || req.auth.staff.role !== "admin") {
            return (0, apiResponse_js_1.sendError)(res, 403, "FORBIDDEN", "Only an admin can create staff accounts.");
        }
    }
    const passwordHash = await bcryptjs_1.default.hash(body.password, 12);
    try {
        await index_js_1.db.insert(schema_js_1.staffUsers).values({
            name: body.name,
            email: body.email,
            passwordHash,
            role: isBootstrapRegistration ? "admin" : body.role,
            isActive: true,
        });
    }
    catch {
        return (0, apiResponse_js_1.sendError)(res, 409, "STAFF_EXISTS", "A staff account with this email already exists.");
    }
    const [staff] = await index_js_1.db
        .select()
        .from(schema_js_1.staffUsers)
        .where((0, drizzle_orm_1.eq)(schema_js_1.staffUsers.email, body.email))
        .limit(1);
    if (!staff)
        return (0, apiResponse_js_1.sendError)(res, 500, "STAFF_REGISTRATION_FAILED", "Could not create staff account.");
    const { delivery, code } = await createAndSendOtp({
        actorType: "staff",
        email: body.email,
        purpose: "signup",
    });
    return (0, apiResponse_js_1.sendSuccess)(res, 201, {
        user: (0, authSerializers_js_1.toPublicStaffUser)(staff),
        otp: {
            email: body.email,
            expiresInMinutes: authSecurity_js_1.OTP_EXPIRES_IN_MINUTES,
            delivery,
            code,
        },
    }, isBootstrapRegistration
        ? "First admin account created. Verify OTP before using the account."
        : "Staff account created. OTP sent for verification.");
};
exports.registerStaff = registerStaff;
const loginStaff = async (req, res) => {
    const body = req.validatedBody;
    const [staff] = await index_js_1.db
        .select()
        .from(schema_js_1.staffUsers)
        .where((0, drizzle_orm_1.eq)(schema_js_1.staffUsers.email, body.email))
        .limit(1);
    const isPasswordValid = staff
        ? await bcryptjs_1.default.compare(body.password, staff.passwordHash)
        : false;
    if (!staff || !staff.isActive || !isPasswordValid) {
        return (0, apiResponse_js_1.sendError)(res, 401, "INVALID_CREDENTIALS", "Invalid email or password.");
    }
    const { delivery, code } = await createAndSendOtp({
        actorType: "staff",
        email: body.email,
        purpose: staff.emailVerifiedAt ? "login" : "signup",
    });
    return sendOtpRequestResponse(res, body.email, delivery, code);
};
exports.loginStaff = loginStaff;
const requestStaffOtp = async (req, res) => {
    const body = req.validatedBody;
    const [staff] = await index_js_1.db
        .select()
        .from(schema_js_1.staffUsers)
        .where((0, drizzle_orm_1.eq)(schema_js_1.staffUsers.email, body.email))
        .limit(1);
    if (!staff || !staff.isActive) {
        return (0, apiResponse_js_1.sendSuccess)(res, 200, {
            email: body.email,
            expiresInMinutes: authSecurity_js_1.OTP_EXPIRES_IN_MINUTES,
            delivery: "email",
        }, "If the staff account exists, an OTP will be sent.");
    }
    const { delivery, code } = await createAndSendOtp({
        actorType: "staff",
        email: body.email,
        purpose: staff.emailVerifiedAt ? "login" : "signup",
    });
    return sendOtpRequestResponse(res, body.email, delivery, code);
};
exports.requestStaffOtp = requestStaffOtp;
const verifyStaffOtp = async (req, res) => {
    const body = req.validatedBody;
    const [staff] = await index_js_1.db
        .select()
        .from(schema_js_1.staffUsers)
        .where((0, drizzle_orm_1.eq)(schema_js_1.staffUsers.email, body.email))
        .limit(1);
    if (!staff || !staff.isActive) {
        return (0, apiResponse_js_1.sendError)(res, 401, "INVALID_OTP", "Invalid or expired OTP.");
    }
    const purpose = staff.emailVerifiedAt ? "login" : "signup";
    const isValid = await consumeValidOtp({
        actorType: "staff",
        email: body.email,
        code: body.code,
        purpose,
    });
    if (!isValid) {
        return (0, apiResponse_js_1.sendError)(res, 401, "INVALID_OTP", "Invalid or expired OTP.");
    }
    const now = new Date();
    await index_js_1.db
        .update(schema_js_1.staffUsers)
        .set({ emailVerifiedAt: staff.emailVerifiedAt ?? now, lastLoginAt: now })
        .where((0, drizzle_orm_1.eq)(schema_js_1.staffUsers.id, staff.id));
    const [updatedStaff] = await index_js_1.db
        .select()
        .from(schema_js_1.staffUsers)
        .where((0, drizzle_orm_1.eq)(schema_js_1.staffUsers.id, staff.id))
        .limit(1);
    if (!updatedStaff)
        return (0, apiResponse_js_1.sendError)(res, 500, "STAFF_AUTH_FAILED", "Could not authenticate staff account.");
    const publicStaff = (0, authSerializers_js_1.toPublicStaffUser)(updatedStaff);
    const session = await (0, sessionService_js_1.createAuthSession)({
        actorType: "staff",
        actorId: publicStaff.id,
        role: publicStaff.role,
        userAgent: getRequestUserAgent(req),
        ipAddress: getRequestIp(req),
    });
    return (0, apiResponse_js_1.sendSuccess)(res, 200, { ...session, user: publicStaff }, "Staff authenticated.");
};
exports.verifyStaffOtp = verifyStaffOtp;
const refreshAuthToken = async (req, res) => {
    const body = req.validatedBody;
    const session = await (0, sessionService_js_1.findValidRefreshSession)(body.refreshToken);
    if (!session) {
        return (0, apiResponse_js_1.sendError)(res, 401, "INVALID_REFRESH_TOKEN", "Invalid or expired refresh token.");
    }
    if (session.actorType === "customer") {
        const [customer] = await index_js_1.db
            .select()
            .from(schema_js_1.customers)
            .where((0, drizzle_orm_1.eq)(schema_js_1.customers.id, session.actorId))
            .limit(1);
        if (!customer) {
            await (0, sessionService_js_1.revokeRefreshSession)(body.refreshToken);
            return (0, apiResponse_js_1.sendError)(res, 401, "INVALID_REFRESH_TOKEN", "Invalid or expired refresh token.");
        }
        const rotated = await (0, sessionService_js_1.rotateRefreshSession)({
            oldRefreshToken: body.refreshToken,
            actorType: "customer",
            actorId: customer.id,
            userAgent: getRequestUserAgent(req),
            ipAddress: getRequestIp(req),
        });
        return (0, apiResponse_js_1.sendSuccess)(res, 200, rotated, "Token refreshed.");
    }
    const [staff] = await index_js_1.db
        .select()
        .from(schema_js_1.staffUsers)
        .where((0, drizzle_orm_1.eq)(schema_js_1.staffUsers.id, session.actorId))
        .limit(1);
    if (!staff || !staff.isActive) {
        await (0, sessionService_js_1.revokeRefreshSession)(body.refreshToken);
        return (0, apiResponse_js_1.sendError)(res, 401, "INVALID_REFRESH_TOKEN", "Invalid or expired refresh token.");
    }
    const rotated = await (0, sessionService_js_1.rotateRefreshSession)({
        oldRefreshToken: body.refreshToken,
        actorType: "staff",
        actorId: staff.id,
        role: staff.role,
        userAgent: getRequestUserAgent(req),
        ipAddress: getRequestIp(req),
    });
    return (0, apiResponse_js_1.sendSuccess)(res, 200, rotated, "Token refreshed.");
};
exports.refreshAuthToken = refreshAuthToken;
const logout = async (req, res) => {
    const body = req.validatedBody;
    await (0, sessionService_js_1.revokeRefreshSession)(body.refreshToken);
    return (0, apiResponse_js_1.sendSuccess)(res, 200, null, "Logged out.");
};
exports.logout = logout;
const logoutAll = async (req, res) => {
    if (!req.auth) {
        return (0, apiResponse_js_1.sendError)(res, 401, "UNAUTHENTICATED", "Authentication required.");
    }
    if (req.auth.actorType === "customer") {
        await (0, sessionService_js_1.revokeAllActorSessions)("customer", req.auth.customer.id);
    }
    else {
        await (0, sessionService_js_1.revokeAllActorSessions)("staff", req.auth.staff.id);
    }
    return (0, apiResponse_js_1.sendSuccess)(res, 200, null, "Logged out from all devices.");
};
exports.logoutAll = logoutAll;
const changePassword = async (req, res) => {
    if (!req.auth || req.auth.actorType !== "staff") {
        return (0, apiResponse_js_1.sendError)(res, 401, "UNAUTHENTICATED", "Staff authentication required.");
    }
    const body = req.validatedBody;
    const [staff] = await index_js_1.db
        .select()
        .from(schema_js_1.staffUsers)
        .where((0, drizzle_orm_1.eq)(schema_js_1.staffUsers.id, req.auth.staff.id))
        .limit(1);
    if (!staff) {
        return (0, apiResponse_js_1.sendError)(res, 404, "STAFF_NOT_FOUND", "Staff account not found.");
    }
    const isCurrentPasswordValid = await bcryptjs_1.default.compare(body.currentPassword, staff.passwordHash);
    if (!isCurrentPasswordValid) {
        return (0, apiResponse_js_1.sendError)(res, 400, "INVALID_CURRENT_PASSWORD", "Current password is incorrect.");
    }
    const newPasswordHash = await bcryptjs_1.default.hash(body.newPassword, 12);
    await index_js_1.db
        .update(schema_js_1.staffUsers)
        .set({ passwordHash: newPasswordHash })
        .where((0, drizzle_orm_1.eq)(schema_js_1.staffUsers.id, staff.id));
    return (0, apiResponse_js_1.sendSuccess)(res, 200, null, "Password updated successfully.");
};
exports.changePassword = changePassword;
const updateCustomerProfile = async (req, res) => {
    if (!req.auth || req.auth.actorType !== "customer") {
        return (0, apiResponse_js_1.sendError)(res, 401, "UNAUTHENTICATED", "Customer authentication required.");
    }
    const body = req.validatedBody;
    const customerId = req.auth.customer.id;
    if (!body.name && !body.phoneNumber) {
        return (0, apiResponse_js_1.sendError)(res, 400, "NO_FIELDS", "At least one field (name or phoneNumber) must be provided.");
    }
    await index_js_1.db
        .update(schema_js_1.customers)
        .set({
        ...(body.name !== undefined ? { name: body.name } : {}),
        ...(body.phoneNumber !== undefined
            ? { phoneNumber: body.phoneNumber }
            : {}),
    })
        .where((0, drizzle_orm_1.eq)(schema_js_1.customers.id, customerId));
    const [updated] = await index_js_1.db
        .select()
        .from(schema_js_1.customers)
        .where((0, drizzle_orm_1.eq)(schema_js_1.customers.id, customerId))
        .limit(1);
    if (!updated) {
        return (0, apiResponse_js_1.sendError)(res, 404, "NOT_FOUND", "Customer not found.");
    }
    return (0, apiResponse_js_1.sendSuccess)(res, 200, (0, authSerializers_js_1.toPublicCustomer)(updated), "Profile updated.");
};
exports.updateCustomerProfile = updateCustomerProfile;
const getCurrentUser = async (req, res) => {
    if (!req.auth)
        return (0, apiResponse_js_1.sendError)(res, 401, "UNAUTHENTICATED", "Authentication required.");
    return (0, apiResponse_js_1.sendSuccess)(res, 200, req.auth);
};
exports.getCurrentUser = getCurrentUser;
//# sourceMappingURL=authController.js.map