// 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.
      // We don't reassign `req.params`, `req.query`, or `req.body` directly, as they
      // might be read-only getters in some environments (like during supertest tests).
      // Instead, we clear the existing object and merge the new properties.
      Object.keys(req.params).forEach(key => delete (req.params as ParamsDictionary)[key]);
      Object.assign(req.params, params);

      Object.keys(req.query).forEach(key => delete (req.query as Query)[key]);
      Object.assign(req.query, query);
      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);
    }
  };