// src/routes/user.routes.ts
import express, { Request, Response, NextFunction } from 'express';
import passport from '../config/passport';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { z } from 'zod';
// Removed: import { logger } from '../services/logger.server';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { UserProfile } from '../types';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { createUploadMiddleware, handleMulterError } from '../middleware/multer.middleware';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { userService } from '../services/userService';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { ForeignKeyConstraintError } from '../services/db/errors.db';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { validateRequest } from '../middleware/validation.middleware';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { validatePasswordStrength } from '../utils/authUtils';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import {
  requiredString,
  numericIdParam,
  optionalNumeric,
  optionalBoolean,
} from '../utils/zodUtils';
import * as db from '../services/db/index.db';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import { cleanupUploadedFile } from '../utils/fileUtils';
// All route handlers now use req.log (request-scoped logger) as per ADR-004
import {
  userUpdateLimiter,
  userSensitiveUpdateLimiter,
  userUploadLimiter,
} from '../config/rateLimiters';
import { sendSuccess, sendNoContent, sendError, ErrorCode } from '../utils/apiResponse';

const router = express.Router();

const updateProfileSchema = z.object({
  body: z
    .object({
      full_name: z.string().optional(),
      avatar_url: z.preprocess(
        (val) => (val === '' ? undefined : val),
        z.string().trim().url().optional(),
      ),
    })
    .refine((data) => Object.keys(data).length > 0, {
      message: 'At least one field to update must be provided.',
    }),
});

const updatePasswordSchema = z.object({
  body: z.object({
    newPassword: z
      .string()
      .trim() // Trim whitespace from password input.
      .min(8, 'Password must be at least 8 characters long.')
      .superRefine((password, ctx) => {
        const strength = validatePasswordStrength(password);
        if (!strength.isValid) ctx.addIssue({ code: 'custom', message: strength.feedback });
      }),
  }),
});

// The `requiredString` utility (modified in `zodUtils.ts`) now handles trimming,
// so no changes are needed here, but we are confirming that password trimming
// is now implicitly handled for this schema.
const deleteAccountSchema = z.object({
  body: z.object({ password: requiredString("Field 'password' is required.") }),
});

const addWatchedItemSchema = z.object({
  body: z.object({
    itemName: requiredString("Field 'itemName' is required."),
    category: requiredString("Field 'category' is required."),
  }),
});

const createShoppingListSchema = z.object({
  body: z.object({ name: requiredString("Field 'name' is required.") }),
});

const createRecipeSchema = z.object({
  body: z.object({
    name: requiredString("Field 'name' is required."),
    instructions: requiredString("Field 'instructions' is required."),
    description: z.string().trim().optional(),
    prep_time_minutes: z.number().int().nonnegative().optional(),
    cook_time_minutes: z.number().int().nonnegative().optional(),
    servings: z.number().int().positive().optional(),
    photo_url: z.string().trim().url().optional(),
  }),
});

// Apply the JWT authentication middleware to all routes in this file.
const notificationQuerySchema = z.object({
  query: z.object({
    limit: optionalNumeric({ default: 20, integer: true, positive: true }),
    offset: optionalNumeric({ default: 0, integer: true, nonnegative: true }),
    includeRead: optionalBoolean({ default: false }),
  }),
});

// An empty schema for routes that do not expect any input, to maintain a consistent validation pattern.
const emptySchema = z.object({});

// Any request to a /api/users/* endpoint will now require a valid JWT.
router.use(passport.authenticate('jwt', { session: false }));

const avatarUpload = createUploadMiddleware({
  storageType: 'avatar',
  fileSize: 1 * 1024 * 1024, // 1MB
  fileFilter: 'image',
});

/**
 * @openapi
 * /users/profile/avatar:
 *   post:
 *     tags: [Users]
 *     summary: Upload user avatar
 *     description: Upload a new avatar image for the authenticated user.
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         multipart/form-data:
 *           schema:
 *             type: object
 *             required:
 *               - avatar
 *             properties:
 *               avatar:
 *                 type: string
 *                 format: binary
 *                 description: Avatar image file (max 1MB)
 *     responses:
 *       200:
 *         description: Avatar updated successfully
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       400:
 *         description: No avatar file uploaded
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/ErrorResponse'
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
router.post(
  '/profile/avatar',
  userUploadLimiter,
  avatarUpload.single('avatar'),
  async (req: Request, res: Response, next: NextFunction) => {
    // The try-catch block was already correct here.
    try {
      // The `requireFileUpload` middleware is not used here, so we must check for `req.file`.
      if (!req.file) return sendError(res, ErrorCode.BAD_REQUEST, 'No avatar file uploaded.', 400);
      const userProfile = req.user as UserProfile;
      const updatedProfile = await userService.updateUserAvatar(
        userProfile.user.user_id,
        req.file,
        req.log,
      );
      sendSuccess(res, updatedProfile);
    } catch (error) {
      // If an error occurs after the file has been uploaded (e.g., DB error),
      // we must clean up the orphaned file from the disk.
      await cleanupUploadedFile(req.file);
      req.log.error({ error }, 'Error uploading avatar');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/notifications:
 *   get:
 *     tags: [Users]
 *     summary: Get user notifications
 *     description: Retrieve notifications for the authenticated user with pagination.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: query
 *         name: limit
 *         schema:
 *           type: integer
 *           default: 20
 *         description: Maximum number of notifications to return
 *       - in: query
 *         name: offset
 *         schema:
 *           type: integer
 *           default: 0
 *         description: Number of notifications to skip
 *       - in: query
 *         name: includeRead
 *         schema:
 *           type: boolean
 *           default: false
 *         description: Include read notifications in results
 *     responses:
 *       200:
 *         description: List of notifications
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
router.get(
  '/notifications',
  validateRequest(notificationQuerySchema),
  async (req: Request, res: Response, next: NextFunction) => {
    // Cast to UserProfile to access user properties safely.
    const userProfile = req.user as UserProfile;
    try {
      const parsedQuery = notificationQuerySchema.shape.query.parse(req.query);
      const notifications = await db.notificationRepo.getNotificationsForUser(
        userProfile.user.user_id,
        parsedQuery.limit!,
        parsedQuery.offset!,
        parsedQuery.includeRead!,
        req.log,
      );
      sendSuccess(res, notifications);
    } catch (error) {
      req.log.error({ error }, 'Error fetching notifications');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/notifications/unread-count:
 *   get:
 *     tags: [Users]
 *     summary: Get unread notification count
 *     description: Get the count of unread notifications for the authenticated user. Optimized for navbar badge UI.
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: Unread notification count
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 success:
 *                   type: boolean
 *                   example: true
 *                 data:
 *                   type: object
 *                   properties:
 *                     count:
 *                       type: integer
 *                       example: 5
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
router.get(
  '/notifications/unread-count',
  validateRequest(emptySchema),
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const userProfile = req.user as UserProfile;
      const count = await db.notificationRepo.getUnreadCount(userProfile.user.user_id, req.log);
      sendSuccess(res, { count });
    } catch (error) {
      req.log.error({ error }, 'Error fetching unread notification count');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/notifications/mark-all-read:
 *   post:
 *     tags: [Users]
 *     summary: Mark all notifications as read
 *     description: Mark all of the authenticated user's notifications as read.
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       204:
 *         description: All notifications marked as read
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
router.post(
  '/notifications/mark-all-read',
  validateRequest(emptySchema),
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const userProfile = req.user as UserProfile;
      await db.notificationRepo.markAllNotificationsAsRead(userProfile.user.user_id, req.log);
      sendNoContent(res);
    } catch (error) {
      req.log.error({ error }, 'Error marking all notifications as read');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/notifications/{notificationId}/mark-read:
 *   post:
 *     tags: [Users]
 *     summary: Mark single notification as read
 *     description: Mark a specific notification as read by its ID.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: notificationId
 *         required: true
 *         schema:
 *           type: integer
 *         description: ID of the notification to mark as read
 *     responses:
 *       204:
 *         description: Notification marked as read
 *       401:
 *         description: Unauthorized - invalid or missing token
 *       404:
 *         description: Notification not found
 */
const notificationIdSchema = numericIdParam('notificationId');
type NotificationIdRequest = z.infer<typeof notificationIdSchema>;
router.post(
  '/notifications/:notificationId/mark-read',
  validateRequest(notificationIdSchema),
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const userProfile = req.user as UserProfile;
      // Apply ADR-003 pattern for type safety
      const { params } = req as unknown as NotificationIdRequest;
      await db.notificationRepo.markNotificationAsRead(
        params.notificationId,
        userProfile.user.user_id,
        req.log,
      );
      sendNoContent(res);
    } catch (error) {
      req.log.error({ error }, 'Error marking notification as read');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/notifications/{notificationId}:
 *   delete:
 *     tags: [Users]
 *     summary: Delete a notification
 *     description: Delete a specific notification by its ID. Users can only delete their own notifications.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: notificationId
 *         required: true
 *         schema:
 *           type: integer
 *         description: ID of the notification to delete
 *     responses:
 *       204:
 *         description: Notification deleted successfully
 *       401:
 *         description: Unauthorized - invalid or missing token
 *       404:
 *         description: Notification not found or user does not have permission
 */
router.delete(
  '/notifications/:notificationId',
  validateRequest(notificationIdSchema),
  async (req: Request, res: Response, next: NextFunction) => {
    try {
      const userProfile = req.user as UserProfile;
      // Apply ADR-003 pattern for type safety
      const { params } = req as unknown as NotificationIdRequest;
      await db.notificationRepo.deleteNotification(
        params.notificationId,
        userProfile.user.user_id,
        req.log,
      );
      sendNoContent(res);
    } catch (error) {
      req.log.error({ error }, 'Error deleting notification');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/profile:
 *   get:
 *     tags: [Users]
 *     summary: Get user profile
 *     description: Retrieve the full profile for the authenticated user.
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: User profile data
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
router.get('/profile', validateRequest(emptySchema), async (req, res, next: NextFunction) => {
  req.log.debug(`[ROUTE] GET /api/users/profile - ENTER`);
  const userProfile = req.user as UserProfile;
  try {
    req.log.debug(
      `[ROUTE] Calling db.userRepo.findUserProfileById for user: ${userProfile.user.user_id}`,
    );
    const fullUserProfile = await db.userRepo.findUserProfileById(
      userProfile.user.user_id,
      req.log,
    );
    sendSuccess(res, fullUserProfile);
  } catch (error) {
    req.log.error({ error }, `[ROUTE] GET /api/users/profile - ERROR`);
    next(error);
  }
});

/**
 * @openapi
 * /users/profile:
 *   put:
 *     tags: [Users]
 *     summary: Update user profile
 *     description: Update the authenticated user's profile information.
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               full_name:
 *                 type: string
 *                 description: User's full name
 *               avatar_url:
 *                 type: string
 *                 format: uri
 *                 description: URL to avatar image
 *     responses:
 *       200:
 *         description: Profile updated successfully
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       400:
 *         description: Validation error - at least one field required
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
type UpdateProfileRequest = z.infer<typeof updateProfileSchema>;
router.put(
  '/profile',
  userUpdateLimiter,
  validateRequest(updateProfileSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] PUT /api/users/profile - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { body } = req as unknown as UpdateProfileRequest;
    try {
      const updatedProfile = await db.userRepo.updateUserProfile(
        userProfile.user.user_id,
        body,
        req.log,
      );
      sendSuccess(res, updatedProfile);
    } catch (error) {
      req.log.error({ error }, `[ROUTE] PUT /api/users/profile - ERROR`);
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/profile/password:
 *   put:
 *     tags: [Users]
 *     summary: Update password
 *     description: Update the authenticated user's password.
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - newPassword
 *             properties:
 *               newPassword:
 *                 type: string
 *                 minLength: 8
 *                 description: New password (must meet strength requirements)
 *     responses:
 *       200:
 *         description: Password updated successfully
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       400:
 *         description: Password validation failed
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
type UpdatePasswordRequest = z.infer<typeof updatePasswordSchema>;
router.put(
  '/profile/password',
  userSensitiveUpdateLimiter,
  validateRequest(updatePasswordSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] PUT /api/users/profile/password - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { body } = req as unknown as UpdatePasswordRequest;

    try {
      await userService.updateUserPassword(userProfile.user.user_id, body.newPassword, req.log);
      sendSuccess(res, { message: 'Password updated successfully.' });
    } catch (error) {
      req.log.error({ error }, `[ROUTE] PUT /api/users/profile/password - ERROR`);
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/account:
 *   delete:
 *     tags: [Users]
 *     summary: Delete user account
 *     description: Permanently delete the authenticated user's account. Requires password confirmation.
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - password
 *             properties:
 *               password:
 *                 type: string
 *                 description: Current password for confirmation
 *     responses:
 *       200:
 *         description: Account deleted successfully
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       401:
 *         description: Unauthorized or incorrect password
 */
type DeleteAccountRequest = z.infer<typeof deleteAccountSchema>;
router.delete(
  '/account',
  userSensitiveUpdateLimiter,
  validateRequest(deleteAccountSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] DELETE /api/users/account - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { body } = req as unknown as DeleteAccountRequest;

    try {
      await userService.deleteUserAccount(userProfile.user.user_id, body.password, req.log);
      sendSuccess(res, { message: 'Account deleted successfully.' });
    } catch (error) {
      req.log.error({ error }, `[ROUTE] DELETE /api/users/account - ERROR`);
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/watched-items:
 *   get:
 *     tags: [Users]
 *     summary: Get watched items
 *     description: Retrieve all items the authenticated user is watching for price changes.
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: List of watched items
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
router.get('/watched-items', validateRequest(emptySchema), async (req, res, next: NextFunction) => {
  req.log.debug(`[ROUTE] GET /api/users/watched-items - ENTER`);
  const userProfile = req.user as UserProfile;
  try {
    const items = await db.personalizationRepo.getWatchedItems(userProfile.user.user_id, req.log);
    sendSuccess(res, items);
  } catch (error) {
    req.log.error({ error }, `[ROUTE] GET /api/users/watched-items - ERROR`);
    next(error);
  }
});

/**
 * @openapi
 * /users/watched-items:
 *   post:
 *     tags: [Users]
 *     summary: Add watched item
 *     description: Add a new item to the user's watchlist for price tracking.
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - itemName
 *               - category
 *             properties:
 *               itemName:
 *                 type: string
 *                 description: Name of the item to watch
 *               category:
 *                 type: string
 *                 description: Category of the item
 *     responses:
 *       201:
 *         description: Item added to watchlist
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       400:
 *         description: Validation error
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
type AddWatchedItemRequest = z.infer<typeof addWatchedItemSchema>;
router.post(
  '/watched-items',
  userUpdateLimiter,
  validateRequest(addWatchedItemSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] POST /api/users/watched-items - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { body } = req as unknown as AddWatchedItemRequest;
    try {
      const newItem = await db.personalizationRepo.addWatchedItem(
        userProfile.user.user_id,
        body.itemName,
        body.category,
        req.log,
      );
      sendSuccess(res, newItem, 201);
    } catch (error) {
      if (error instanceof ForeignKeyConstraintError) {
        return sendError(res, ErrorCode.BAD_REQUEST, error.message, 400);
      }
      req.log.error({ error, body: req.body }, 'Failed to add watched item');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/watched-items/{masterItemId}:
 *   delete:
 *     tags: [Users]
 *     summary: Remove watched item
 *     description: Remove an item from the user's watchlist.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: masterItemId
 *         required: true
 *         schema:
 *           type: integer
 *         description: ID of the master item to stop watching
 *     responses:
 *       204:
 *         description: Item removed from watchlist
 *       401:
 *         description: Unauthorized - invalid or missing token
 *       404:
 *         description: Item not found in watchlist
 */
const watchedItemIdSchema = numericIdParam('masterItemId');
type DeleteWatchedItemRequest = z.infer<typeof watchedItemIdSchema>;
router.delete(
  '/watched-items/:masterItemId',
  userUpdateLimiter,
  validateRequest(watchedItemIdSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] DELETE /api/users/watched-items/:masterItemId - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { params } = req as unknown as DeleteWatchedItemRequest;
    try {
      await db.personalizationRepo.removeWatchedItem(
        userProfile.user.user_id,
        params.masterItemId,
        req.log,
      );
      sendNoContent(res);
    } catch (error) {
      req.log.error({ error }, `[ROUTE] DELETE /api/users/watched-items/:masterItemId - ERROR`);
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/shopping-lists:
 *   get:
 *     tags: [Users]
 *     summary: Get shopping lists
 *     description: Retrieve all shopping lists for the authenticated user.
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: List of shopping lists
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
router.get(
  '/shopping-lists',
  validateRequest(emptySchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] GET /api/users/shopping-lists - ENTER`);
    const userProfile = req.user as UserProfile;
    try {
      const lists = await db.shoppingRepo.getShoppingLists(userProfile.user.user_id, req.log);
      sendSuccess(res, lists);
    } catch (error) {
      req.log.error({ error }, `[ROUTE] GET /api/users/shopping-lists - ERROR`);
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/shopping-lists/{listId}:
 *   get:
 *     tags: [Users]
 *     summary: Get shopping list by ID
 *     description: Retrieve a specific shopping list with all its items.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: listId
 *         required: true
 *         schema:
 *           type: integer
 *         description: Shopping list ID
 *     responses:
 *       200:
 *         description: Shopping list with items
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       401:
 *         description: Unauthorized - invalid or missing token
 *       404:
 *         description: Shopping list not found
 */
const shoppingListIdSchema = numericIdParam('listId');
type GetShoppingListRequest = z.infer<typeof shoppingListIdSchema>;
router.get(
  '/shopping-lists/:listId',
  validateRequest(shoppingListIdSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] GET /api/users/shopping-lists/:listId - ENTER`);
    const userProfile = req.user as UserProfile;
    const { params } = req as unknown as GetShoppingListRequest;
    try {
      const list = await db.shoppingRepo.getShoppingListById(
        params.listId,
        userProfile.user.user_id,
        req.log,
      );
      sendSuccess(res, list);
    } catch (error) {
      req.log.error(
        { error, listId: params.listId },
        `[ROUTE] GET /api/users/shopping-lists/:listId - ERROR`,
      );
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/shopping-lists:
 *   post:
 *     tags: [Users]
 *     summary: Create shopping list
 *     description: Create a new shopping list for the authenticated user.
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - name
 *             properties:
 *               name:
 *                 type: string
 *                 description: Name of the shopping list
 *     responses:
 *       201:
 *         description: Shopping list created
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       400:
 *         description: Validation error
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
type CreateShoppingListRequest = z.infer<typeof createShoppingListSchema>;
router.post(
  '/shopping-lists',
  userUpdateLimiter,
  validateRequest(createShoppingListSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] POST /api/users/shopping-lists - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { body } = req as unknown as CreateShoppingListRequest;
    try {
      const newList = await db.shoppingRepo.createShoppingList(
        userProfile.user.user_id,
        body.name,
        req.log,
      );
      sendSuccess(res, newList, 201);
    } catch (error) {
      if (error instanceof ForeignKeyConstraintError) {
        return sendError(res, ErrorCode.BAD_REQUEST, error.message, 400);
      }
      req.log.error({ error, body: req.body }, 'Failed to create shopping list');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/shopping-lists/{listId}:
 *   delete:
 *     tags: [Users]
 *     summary: Delete shopping list
 *     description: Delete a shopping list and all its items.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: listId
 *         required: true
 *         schema:
 *           type: integer
 *         description: Shopping list ID
 *     responses:
 *       204:
 *         description: Shopping list deleted
 *       401:
 *         description: Unauthorized - invalid or missing token
 *       404:
 *         description: Shopping list not found
 */
router.delete(
  '/shopping-lists/:listId',
  userUpdateLimiter,
  validateRequest(shoppingListIdSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] DELETE /api/users/shopping-lists/:listId - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { params } = req as unknown as GetShoppingListRequest;
    try {
      await db.shoppingRepo.deleteShoppingList(params.listId, userProfile.user.user_id, req.log);
      sendNoContent(res);
    } catch (error: unknown) {
      const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred';
      req.log.error(
        { errorMessage, params: req.params },
        `[ROUTE] DELETE /api/users/shopping-lists/:listId - ERROR`,
      );
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/shopping-lists/{listId}/items:
 *   post:
 *     tags: [Users]
 *     summary: Add item to shopping list
 *     description: Add an item to a shopping list by master item ID or custom name.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: listId
 *         required: true
 *         schema:
 *           type: integer
 *         description: Shopping list ID
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               masterItemId:
 *                 type: integer
 *                 description: ID of master item to add
 *               customItemName:
 *                 type: string
 *                 description: Custom item name (use if masterItemId not provided)
 *     responses:
 *       201:
 *         description: Item added to shopping list
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       400:
 *         description: Validation error - must provide masterItemId or customItemName
 *       401:
 *         description: Unauthorized - invalid or missing token
 *       404:
 *         description: Shopping list not found
 */
const addShoppingListItemSchema = shoppingListIdSchema.extend({
  body: z
    .object({
      masterItemId: z.number().int().positive().optional(),
      customItemName: z
        .string()
        .trim()
        .min(1, 'customItemName cannot be empty if provided')
        .optional(),
    })
    .refine((data) => data.masterItemId || data.customItemName, {
      message: 'Either masterItemId or customItemName must be provided.',
    }),
});
type AddShoppingListItemRequest = z.infer<typeof addShoppingListItemSchema>;
router.post(
  '/shopping-lists/:listId/items',
  userUpdateLimiter,
  validateRequest(addShoppingListItemSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] POST /api/users/shopping-lists/:listId/items - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { params, body } = req as unknown as AddShoppingListItemRequest;
    try {
      const newItem = await db.shoppingRepo.addShoppingListItem(
        params.listId,
        userProfile.user.user_id,
        body,
        req.log,
      );
      sendSuccess(res, newItem, 201);
    } catch (error) {
      if (error instanceof ForeignKeyConstraintError) {
        return sendError(res, ErrorCode.BAD_REQUEST, error.message, 400);
      }
      req.log.error(
        { error, params: req.params, body: req.body },
        'Failed to add shopping list item',
      );
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/shopping-lists/items/{itemId}:
 *   put:
 *     tags: [Users]
 *     summary: Update shopping list item
 *     description: Update quantity or purchased status of a shopping list item.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: itemId
 *         required: true
 *         schema:
 *           type: integer
 *         description: Shopping list item ID
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               quantity:
 *                 type: integer
 *                 minimum: 0
 *                 description: Item quantity
 *               is_purchased:
 *                 type: boolean
 *                 description: Whether item has been purchased
 *     responses:
 *       200:
 *         description: Item updated
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       400:
 *         description: Validation error - at least one field required
 *       401:
 *         description: Unauthorized - invalid or missing token
 *       404:
 *         description: Item not found
 */
const updateShoppingListItemSchema = numericIdParam('itemId').extend({
  body: z
    .object({
      quantity: z.number().int().nonnegative().optional(),
      is_purchased: z.boolean().optional(),
    })
    .refine((data) => Object.keys(data).length > 0, {
      message: 'At least one field (quantity, is_purchased) must be provided.',
    }),
});
type UpdateShoppingListItemRequest = z.infer<typeof updateShoppingListItemSchema>;
router.put(
  '/shopping-lists/items/:itemId',
  userUpdateLimiter,
  validateRequest(updateShoppingListItemSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] PUT /api/users/shopping-lists/items/:itemId - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { params, body } = req as unknown as UpdateShoppingListItemRequest;
    try {
      const updatedItem = await db.shoppingRepo.updateShoppingListItem(
        params.itemId,
        userProfile.user.user_id,
        body,
        req.log,
      );
      sendSuccess(res, updatedItem);
    } catch (error: unknown) {
      req.log.error(
        { error, params: req.params, body: req.body },
        `[ROUTE] PUT /api/users/shopping-lists/items/:itemId - ERROR`,
      );
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/shopping-lists/items/{itemId}:
 *   delete:
 *     tags: [Users]
 *     summary: Remove shopping list item
 *     description: Remove an item from a shopping list.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: itemId
 *         required: true
 *         schema:
 *           type: integer
 *         description: Shopping list item ID
 *     responses:
 *       204:
 *         description: Item removed from shopping list
 *       401:
 *         description: Unauthorized - invalid or missing token
 *       404:
 *         description: Item not found
 */
const shoppingListItemIdSchema = numericIdParam('itemId');
type DeleteShoppingListItemRequest = z.infer<typeof shoppingListItemIdSchema>;
router.delete(
  '/shopping-lists/items/:itemId',
  userUpdateLimiter,
  validateRequest(shoppingListItemIdSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] DELETE /api/users/shopping-lists/items/:itemId - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { params } = req as unknown as DeleteShoppingListItemRequest;
    try {
      await db.shoppingRepo.removeShoppingListItem(
        params.itemId,
        userProfile.user.user_id,
        req.log,
      );
      sendNoContent(res);
    } catch (error: unknown) {
      req.log.error(
        { error, params: req.params },
        `[ROUTE] DELETE /api/users/shopping-lists/items/:itemId - ERROR`,
      );
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/profile/preferences:
 *   put:
 *     tags: [Users]
 *     summary: Update user preferences
 *     description: Update the authenticated user's application preferences.
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             additionalProperties: true
 *             description: User preference key-value pairs
 *     responses:
 *       200:
 *         description: Preferences updated
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
const updatePreferencesSchema = z.object({
  body: z.object({}).passthrough(), // Ensures body is an object, allows any properties
});
type UpdatePreferencesRequest = z.infer<typeof updatePreferencesSchema>;
router.put(
  '/profile/preferences',
  userUpdateLimiter,
  validateRequest(updatePreferencesSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] PUT /api/users/profile/preferences - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { body } = req as unknown as UpdatePreferencesRequest;
    try {
      const updatedProfile = await db.userRepo.updateUserPreferences(
        userProfile.user.user_id,
        body,
        req.log,
      );
      sendSuccess(res, updatedProfile);
    } catch (error) {
      req.log.error({ error }, `[ROUTE] PUT /api/users/profile/preferences - ERROR`);
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/me/dietary-restrictions:
 *   get:
 *     tags: [Users]
 *     summary: Get dietary restrictions
 *     description: Retrieve the authenticated user's dietary restrictions.
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: List of dietary restrictions
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
router.get(
  '/me/dietary-restrictions',
  validateRequest(emptySchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] GET /api/users/me/dietary-restrictions - ENTER`);
    const userProfile = req.user as UserProfile;
    try {
      const restrictions = await db.personalizationRepo.getUserDietaryRestrictions(
        userProfile.user.user_id,
        req.log,
      );
      sendSuccess(res, restrictions);
    } catch (error) {
      req.log.error({ error }, `[ROUTE] GET /api/users/me/dietary-restrictions - ERROR`);
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/me/dietary-restrictions:
 *   put:
 *     tags: [Users]
 *     summary: Set dietary restrictions
 *     description: Replace the authenticated user's dietary restrictions.
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - restrictionIds
 *             properties:
 *               restrictionIds:
 *                 type: array
 *                 items:
 *                   type: integer
 *                 description: Array of dietary restriction IDs
 *     responses:
 *       204:
 *         description: Dietary restrictions updated
 *       400:
 *         description: Invalid restriction IDs
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
const setUserRestrictionsSchema = z.object({
  body: z.object({ restrictionIds: z.array(z.number().int().positive()) }),
});
type SetUserRestrictionsRequest = z.infer<typeof setUserRestrictionsSchema>;
router.put(
  '/me/dietary-restrictions',
  userUpdateLimiter,
  validateRequest(setUserRestrictionsSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] PUT /api/users/me/dietary-restrictions - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { body } = req as unknown as SetUserRestrictionsRequest;
    try {
      await db.personalizationRepo.setUserDietaryRestrictions(
        userProfile.user.user_id,
        body.restrictionIds,
        req.log,
      );
      sendNoContent(res);
    } catch (error) {
      if (error instanceof ForeignKeyConstraintError) {
        return sendError(res, ErrorCode.BAD_REQUEST, error.message, 400);
      }
      req.log.error({ error, body: req.body }, 'Failed to set user dietary restrictions');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/me/appliances:
 *   get:
 *     tags: [Users]
 *     summary: Get user appliances
 *     description: Retrieve the authenticated user's kitchen appliances.
 *     security:
 *       - bearerAuth: []
 *     responses:
 *       200:
 *         description: List of user's appliances
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
router.get('/me/appliances', validateRequest(emptySchema), async (req, res, next: NextFunction) => {
  req.log.debug(`[ROUTE] GET /api/users/me/appliances - ENTER`);
  const userProfile = req.user as UserProfile;
  try {
    const appliances = await db.personalizationRepo.getUserAppliances(
      userProfile.user.user_id,
      req.log,
    );
    sendSuccess(res, appliances);
  } catch (error) {
    req.log.error({ error }, `[ROUTE] GET /api/users/me/appliances - ERROR`);
    next(error);
  }
});

/**
 * @openapi
 * /users/me/appliances:
 *   put:
 *     tags: [Users]
 *     summary: Set user appliances
 *     description: Replace the authenticated user's kitchen appliances.
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - applianceIds
 *             properties:
 *               applianceIds:
 *                 type: array
 *                 items:
 *                   type: integer
 *                 description: Array of appliance IDs
 *     responses:
 *       204:
 *         description: Appliances updated
 *       400:
 *         description: Invalid appliance IDs
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
const setUserAppliancesSchema = z.object({
  body: z.object({ applianceIds: z.array(z.number().int().positive()) }),
});
type SetUserAppliancesRequest = z.infer<typeof setUserAppliancesSchema>;
router.put(
  '/me/appliances',
  userUpdateLimiter,
  validateRequest(setUserAppliancesSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] PUT /api/users/me/appliances - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { body } = req as unknown as SetUserAppliancesRequest;
    try {
      await db.personalizationRepo.setUserAppliances(
        userProfile.user.user_id,
        body.applianceIds,
        req.log,
      );
      sendNoContent(res);
    } catch (error) {
      if (error instanceof ForeignKeyConstraintError) {
        return sendError(res, ErrorCode.BAD_REQUEST, error.message, 400);
      }
      req.log.error({ error, body: req.body }, 'Failed to set user appliances');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/addresses/{addressId}:
 *   get:
 *     tags: [Users]
 *     summary: Get address by ID
 *     description: Retrieve a specific address by its ID. Users can only access their own addresses.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: addressId
 *         required: true
 *         schema:
 *           type: integer
 *         description: Address ID
 *     responses:
 *       200:
 *         description: Address details
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       401:
 *         description: Unauthorized - invalid or missing token
 *       404:
 *         description: Address not found
 */
const addressIdSchema = numericIdParam('addressId');
type GetAddressRequest = z.infer<typeof addressIdSchema>;
router.get(
  '/addresses/:addressId',
  validateRequest(addressIdSchema),
  async (req, res, next: NextFunction) => {
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { params } = req as unknown as GetAddressRequest;
    try {
      const addressId = params.addressId;
      const address = await userService.getUserAddress(userProfile, addressId, req.log);
      sendSuccess(res, address);
    } catch (error) {
      req.log.error({ error }, 'Error fetching user address');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/profile/address:
 *   put:
 *     tags: [Users]
 *     summary: Create or update address
 *     description: Create or update the authenticated user's primary address.
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               address_line_1:
 *                 type: string
 *                 description: Street address line 1
 *               address_line_2:
 *                 type: string
 *                 description: Street address line 2 (apt, suite, etc.)
 *               city:
 *                 type: string
 *                 description: City name
 *               province_state:
 *                 type: string
 *                 description: Province or state
 *               postal_code:
 *                 type: string
 *                 description: Postal or ZIP code
 *               country:
 *                 type: string
 *                 description: Country name
 *     responses:
 *       200:
 *         description: Address updated successfully
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       400:
 *         description: Validation error - at least one field required
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
const updateUserAddressSchema = z.object({
  body: z
    .object({
      address_line_1: z.string().trim().optional(),
      address_line_2: z.string().trim().optional(),
      city: z.string().trim().optional(),
      province_state: z.string().trim().optional(),
      postal_code: z.string().trim().optional(),
      country: z.string().trim().optional(),
    })
    .refine((data) => Object.keys(data).length > 0, {
      message: 'At least one address field must be provided.',
    }),
});
type UpdateUserAddressRequest = z.infer<typeof updateUserAddressSchema>;
router.put(
  '/profile/address',
  userUpdateLimiter,
  validateRequest(updateUserAddressSchema),
  async (req, res, next: NextFunction) => {
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { body: addressData } = req as unknown as UpdateUserAddressRequest;

    try {
      // Per ADR-002, complex operations involving multiple database writes should be
      // encapsulated in a single service method that manages the transaction.
      // This ensures both the address upsert and the user profile update are atomic.
      const addressId = await userService.upsertUserAddress(userProfile, addressData, req.log); // This was a duplicate, fixed.
      sendSuccess(res, { message: 'Address updated successfully', address_id: addressId });
    } catch (error) {
      req.log.error({ error }, 'Error updating user address');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/recipes:
 *   post:
 *     tags: [Users]
 *     summary: Create recipe
 *     description: Create a new recipe for the authenticated user.
 *     security:
 *       - bearerAuth: []
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             required:
 *               - name
 *               - instructions
 *             properties:
 *               name:
 *                 type: string
 *                 description: Recipe name
 *               instructions:
 *                 type: string
 *                 description: Cooking instructions
 *               description:
 *                 type: string
 *                 description: Recipe description
 *               prep_time_minutes:
 *                 type: integer
 *                 minimum: 0
 *                 description: Preparation time in minutes
 *               cook_time_minutes:
 *                 type: integer
 *                 minimum: 0
 *                 description: Cooking time in minutes
 *               servings:
 *                 type: integer
 *                 minimum: 1
 *                 description: Number of servings
 *               photo_url:
 *                 type: string
 *                 format: uri
 *                 description: URL to recipe photo
 *     responses:
 *       201:
 *         description: Recipe created
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       400:
 *         description: Validation error
 *       401:
 *         description: Unauthorized - invalid or missing token
 */
router.post(
  '/recipes',
  userUpdateLimiter,
  validateRequest(createRecipeSchema),
  async (req, res, next) => {
    const userProfile = req.user as UserProfile;
    const { body } = req as unknown as z.infer<typeof createRecipeSchema>;
    try {
      const recipe = await db.recipeRepo.createRecipe(userProfile.user.user_id, body, req.log);
      sendSuccess(res, recipe, 201);
    } catch (error) {
      req.log.error({ error }, 'Error creating recipe');
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/recipes/{recipeId}:
 *   delete:
 *     tags: [Users]
 *     summary: Delete recipe
 *     description: Delete a recipe created by the authenticated user.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: recipeId
 *         required: true
 *         schema:
 *           type: integer
 *         description: Recipe ID
 *     responses:
 *       204:
 *         description: Recipe deleted
 *       401:
 *         description: Unauthorized - invalid or missing token
 *       404:
 *         description: Recipe not found or not owned by user
 */
const recipeIdSchema = numericIdParam('recipeId');
type DeleteRecipeRequest = z.infer<typeof recipeIdSchema>;
router.delete(
  '/recipes/:recipeId',
  userUpdateLimiter,
  validateRequest(recipeIdSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] DELETE /api/users/recipes/:recipeId - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { params } = req as unknown as DeleteRecipeRequest;
    try {
      await db.recipeRepo.deleteRecipe(params.recipeId, userProfile.user.user_id, false, req.log);
      sendNoContent(res);
    } catch (error) {
      req.log.error(
        { error, params: req.params },
        `[ROUTE] DELETE /api/users/recipes/:recipeId - ERROR`,
      );
      next(error);
    }
  },
);

/**
 * @openapi
 * /users/recipes/{recipeId}:
 *   put:
 *     tags: [Users]
 *     summary: Update recipe
 *     description: Update a recipe created by the authenticated user.
 *     security:
 *       - bearerAuth: []
 *     parameters:
 *       - in: path
 *         name: recipeId
 *         required: true
 *         schema:
 *           type: integer
 *         description: Recipe ID
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema:
 *             type: object
 *             properties:
 *               name:
 *                 type: string
 *                 description: Recipe name
 *               description:
 *                 type: string
 *                 description: Recipe description
 *               instructions:
 *                 type: string
 *                 description: Cooking instructions
 *               prep_time_minutes:
 *                 type: integer
 *                 description: Preparation time in minutes
 *               cook_time_minutes:
 *                 type: integer
 *                 description: Cooking time in minutes
 *               servings:
 *                 type: integer
 *                 description: Number of servings
 *               photo_url:
 *                 type: string
 *                 format: uri
 *                 description: URL to recipe photo
 *     responses:
 *       200:
 *         description: Recipe updated
 *         content:
 *           application/json:
 *             schema:
 *               $ref: '#/components/schemas/SuccessResponse'
 *       400:
 *         description: Validation error - at least one field required
 *       401:
 *         description: Unauthorized - invalid or missing token
 *       404:
 *         description: Recipe not found or not owned by user
 */
const updateRecipeSchema = recipeIdSchema.extend({
  body: z
    .object({
      name: z.string().trim().optional(),
      description: z.string().trim().optional(),
      instructions: z.string().trim().optional(),
      prep_time_minutes: z.number().int().optional(),
      cook_time_minutes: z.number().int().optional(),
      servings: z.number().int().optional(),
      photo_url: z.string().trim().url().optional(),
    })
    .refine((data) => Object.keys(data).length > 0, { message: 'No fields provided to update.' }),
});
type UpdateRecipeRequest = z.infer<typeof updateRecipeSchema>;
router.put(
  '/recipes/:recipeId',
  userUpdateLimiter,
  validateRequest(updateRecipeSchema),
  async (req, res, next: NextFunction) => {
    req.log.debug(`[ROUTE] PUT /api/users/recipes/:recipeId - ENTER`);
    const userProfile = req.user as UserProfile;
    // Apply ADR-003 pattern for type safety
    const { params, body } = req as unknown as UpdateRecipeRequest;

    try {
      const updatedRecipe = await db.recipeRepo.updateRecipe(
        params.recipeId,
        userProfile.user.user_id,
        body,
        req.log,
      );
      sendSuccess(res, updatedRecipe);
    } catch (error) {
      req.log.error(
        { error, params: req.params, body: req.body },
        `[ROUTE] PUT /api/users/recipes/:recipeId - ERROR`,
      );
      next(error);
    }
  },
);

/* Catches errors from multer (e.g., file size, file filter) */
router.use(handleMulterError);

export default router;
