// src/middleware/validation.middleware.ts
import { Request, Response, NextFunction } from 'express';
import { ParamsDictionary, Query } from 'express-serve-static-core';
import { ZodObject, ZodError, z } from 'zod';
import { ValidationError } from '../services/db/errors.db';

/**
 * A middleware factory that generates a validation middleware for a given Zod schema.
 * It validates the request's params, query, and body against the schema.
 *
 * @param schema - The Zod schema to validate against.
 * @returns An Express middleware function.
 */
export const validateRequest =
  (schema: ZodObject<z.ZodRawShape>) => async (req: Request, res: Response, next: NextFunction) => {
    try {
      // Parse and validate the request parts against the schema.
      // This will throw a ZodError if validation fails.
      const { params, query, body } = await schema.parseAsync({
        params: req.params,
        query: req.query,
        body: req.body,
      });

      // On success, merge the parsed (and coerced) data back into the request objects.
      // For req.params, we can delete existing keys and assign new ones.
      Object.keys(req.params).forEach((key) => delete (req.params as ParamsDictionary)[key]);
      Object.assign(req.params, params);

      // For req.query in Express 5, the query object is lazily evaluated from the URL
      // and cannot be mutated directly. We use Object.defineProperty to replace
      // the getter with our validated/transformed query object.
      Object.defineProperty(req, 'query', {
        value: query as Query,
        writable: true,
        configurable: true,
        enumerable: true,
      });

      // For body, direct reassignment works.
      req.body = body;

      return next();
    } catch (error) {
      if (error instanceof ZodError) {
        // If it's a Zod validation error, wrap it in our custom ValidationError.
        // The global errorHandler will format this into a 400 response.
        // We must map Zod's issues to our custom ValidationIssue format, as Zod's
        // `path` can include `symbol`, which our type does not allow.
        const validationIssues = error.issues.map((issue) => ({
          ...issue,
          // Convert any symbols in the path to strings to match our ValidationIssue type.
          path: issue.path.map((p) => String(p)),
        }));

        return next(new ValidationError(validationIssues));
      }
      // For any other unexpected errors, pass them on.
      return next(error);
    }
  };
