import { Request, Response, Router, NextFunction } from 'express';
import { Document, Model, Types } from 'mongoose';

// ─── Exported Type Aliases ───────────────────────────────────────────────────

/** Express middleware function signature. */
export type MiddlewareFunction = (req: Request, res: Response, next: NextFunction) => void;

/** Success callback invoked after a successful operation. */
export type SuccessHandler<T> = (
  res: Response,
  method: string,
  result: T | T[] | any,
  meta?: PaginationMeta
) => void;

/** Error callback invoked when an operation fails. */
export type ErrorHandler = (res: Response, method: string, error: Error) => void;

/** Validation result returned by validate hooks. */
export interface ValidationResult {
  valid: boolean;
  errors?: string[];
}

/** Pagination metadata included in list responses. */
export interface PaginationMeta {
  total: number;
  page: number;
  limit: number;
  pages: number;
  hasNext: boolean;
  hasPrev: boolean;
}

/** Allowed HTTP methods for the CRUD controller. */
export type HttpMethod = 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE';

// ─── CrudOptions Interface ──────────────────────────────────────────────────

/**
 * Configuration options for CrudController.
 *
 * Every option is optional — the controller works with zero configuration,
 * but each option unlocks additional flexibility.
 */
export interface CrudOptions<T extends Document> {
  // ── Core ──────────────────────────────────────────────────────────────

  /**
   * HTTP methods to enable. Defaults to all five.
   * @default ['POST', 'GET', 'PUT', 'PATCH', 'DELETE']
   */
  methods?: HttpMethod[];

  /**
   * Global middleware applied to **every** generated route.
   * For per-operation middleware, use `routeMiddleware` instead.
   */
  middleware?: MiddlewareFunction[];

  /**
   * Per-operation middleware — lets you apply different middleware
   * to different CRUD operations (e.g. only admins can delete).
   *
   * @example
   * routeMiddleware: {
   *   delete: [requireAdmin],
   *   create: [validateBody],
   * }
   */
  routeMiddleware?: {
    create?: MiddlewareFunction[];
    read?: MiddlewareFunction[];
    update?: MiddlewareFunction[];
    delete?: MiddlewareFunction[];
  };

  // ── Response Callbacks ────────────────────────────────────────────────

  /**
   * Custom success response handler.
   * The 4th argument `meta` is provided on paginated list endpoints.
   */
  onSuccess?: SuccessHandler<T>;

  /** Custom error response handler. */
  onError?: ErrorHandler;

  // ── Lifecycle Hooks ───────────────────────────────────────────────────

  /**
   * Lifecycle hooks that run before/after each operation.
   * `before*` hooks can transform data by returning a modified object.
   * `after*` hooks are for side-effects (logging, events, notifications).
   *
   * @example
   * hooks: {
   *   beforeCreate: async (req, data) => {
   *     data.createdBy = req.user.id;
   *     return data;
   *   },
   *   afterDelete: async (req, result) => {
   *     await auditLog('delete', result._id);
   *   },
   * }
   */
  hooks?: {
    beforeCreate?: (req: Request, data: any) => Promise<any> | any;
    afterCreate?: (req: Request, result: T) => Promise<void> | void;
    beforeUpdate?: (req: Request, id: string, data: any) => Promise<any> | any;
    afterUpdate?: (req: Request, result: T) => Promise<void> | void;
    beforeDelete?: (req: Request, id: string) => Promise<void> | void;
    afterDelete?: (req: Request, result: T) => Promise<void> | void;
    beforeRead?: (req: Request, query: any) => Promise<any> | any;
    afterRead?: (req: Request, result: T | T[]) => Promise<T | T[]> | (T | T[]);
  };

  // ── Validation ────────────────────────────────────────────────────────

  /**
   * Validation hooks that run **before** Mongoose validation.
   * Return `{ valid: false, errors: [...] }` to reject early with a 400.
   *
   * @example
   * validate: {
   *   create: (data) => ({
   *     valid: !!data.email,
   *     errors: data.email ? [] : ['Email is required'],
   *   }),
   * }
   */
  validate?: {
    create?: (data: any) => ValidationResult;
    update?: (data: any) => ValidationResult;
  };

  // ── Query Features ────────────────────────────────────────────────────

  /**
   * Default fields to return (Mongoose select syntax).
   * Can be overridden per-request via `?select=name,email`.
   * @example "name email -password"
   */
  select?: string;

  /**
   * Auto-populate references on read operations.
   * Can be overridden per-request via `?populate=author,comments`.
   * @example "author" or ["author", { path: "comments", select: "text" }]
   */
  populate?: string | object | (string | object)[];

  /**
   * Fields to search across when using the `/search` endpoint.
   * Uses case-insensitive `$regex` matching.
   * @example ['name', 'email', 'description']
   */
  searchFields?: string[];

  // ── Soft Delete ───────────────────────────────────────────────────────

  /**
   * When `true`, DELETE operations set `deletedAt: new Date()` instead of
   * removing the document. GET operations auto-exclude soft-deleted records
   * unless `?includeDeleted=true` is passed.
   *
   * A restore endpoint `PATCH /endpoint/:id/restore` is also created.
   * @default false
   */
  softDelete?: boolean;

  // ── Aggregation ───────────────────────────────────────────────────────

  /**
   * MongoDB aggregation pipeline stages, or a function that receives the
   * request and returns pipeline stages (for dynamic pipelines).
   *
   * @example
   * // Static pipeline
   * aggregatePipeline: [{ $match: { status: 'Active' } }]
   *
   * // Dynamic pipeline
   * aggregatePipeline: (req) => [
   *   { $match: { region: req.query.region } },
   * ]
   */
  aggregatePipeline?: object[] | ((req: Request) => object[]);

  // ── Related Model ─────────────────────────────────────────────────────

  /** Related Mongoose model for cascading operations. */
  relatedModel?: Model<any>;

  /** Field name linking the related model to this model. */
  relatedField?: string;

  /** HTTP methods to cascade to the related model. */
  relatedMethods?: HttpMethod[];

  // ── Custom Routes ─────────────────────────────────────────────────────

  /**
   * Additional custom routes beyond standard CRUD.
   * Custom routes are **always** registered regardless of `methods` filter.
   *
   * @example
   * customRoutes: [{
   *   method: 'get',
   *   path: '/stats',
   *   handler: async (req, res) => {
   *     const count = await Model.countDocuments();
   *     res.json({ count });
   *   },
   * }]
   */
  customRoutes?: {
    method: 'post' | 'get' | 'put' | 'patch' | 'delete';
    path: string;
    middleware?: MiddlewareFunction[];
    handler: (req: Request, res: Response) => void;
  }[];
}

// ─── Route Info ─────────────────────────────────────────────────────────────

export interface RouteInfo {
  method: string;
  path: string;
  params?: string[];
}

// ─── CrudController Class ───────────────────────────────────────────────────

/**
 * A powerful, flexible CRUD Controller for Express + Mongoose.
 *
 * Automatically generates RESTful endpoints for any Mongoose model with
 * support for lifecycle hooks, validation, soft delete, bulk operations,
 * search, field selection, population, and more.
 *
 * @example
 * const ctrl = new CrudController(UserModel, 'users', {
 *   methods: ['GET', 'POST', 'PATCH', 'DELETE'],
 *   softDelete: true,
 *   searchFields: ['name', 'email'],
 *   hooks: {
 *     beforeCreate: (req, data) => ({ ...data, createdBy: req.user.id }),
 *   },
 * });
 * app.use('/api', ctrl.getRouter());
 */
class CrudController<T extends Document> {
  private model: Model<T>;
  private endpoint: string;
  private router: Router;
  private routes: RouteInfo[];

  constructor(model: Model<T>, endpoint: string, options: CrudOptions<T> = {}) {
    this.model = model;
    this.endpoint = endpoint;
    this.router = Router();
    this.routes = [];
    this.configureRoutes(options);
  }

  // ── Helpers ─────────────────────────────────────────────────────────────

  /**
   * Merges global middleware + per-operation middleware + handler into a single array.
   */
  private buildMiddlewareChain(
    handler: (req: Request, res: Response) => void,
    globalMiddleware: MiddlewareFunction[],
    operationMiddleware?: MiddlewareFunction[]
  ): (MiddlewareFunction | ((req: Request, res: Response) => void))[] {
    return [...globalMiddleware, ...(operationMiddleware || []), handler];
  }

  /** Records a route in the internal registry. */
  private registerRoute(method: string, path: string, params?: string[]): void {
    this.routes.push({ method, path, params });
  }

  /** Parses a `?select=name,email` query param into Mongoose select syntax. */
  private parseSelect(querySelect: string | undefined, defaultSelect?: string): string | undefined {
    if (querySelect) return (querySelect as string).split(',').join(' ');
    return defaultSelect;
  }

  /** Parses a `?populate=author,comments` query param. */
  private parsePopulate(
    queryPopulate: string | undefined,
    defaultPopulate?: string | object | (string | object)[]
  ): any {
    if (queryPopulate) {
      return (queryPopulate as string).split(',').map((p) => p.trim());
    }
    return defaultPopulate;
  }

  /** Safely parses JSON from a query param, returns fallback on failure. */
  private safeJsonParse(value: string | undefined, fallback: any = {}): any {
    if (!value) return fallback;
    try {
      return JSON.parse(value);
    } catch {
      return fallback;
    }
  }

  /** Builds the soft-delete filter to exclude deleted records. */
  private softDeleteFilter(req: Request, softDelete: boolean): object {
    if (!softDelete) return {};
    const includeDeleted = req.query.includeDeleted === 'true';
    return includeDeleted ? {} : { deletedAt: { $exists: false } };
  }

  // ── Route Configuration ────────────────────────────────────────────────

  private configureRoutes(options: CrudOptions<T>): void {
    const {
      middleware = [],
      routeMiddleware = {},
      onSuccess = (res, method, result, meta) => {
        if (meta) {
          res.status(200).json({ data: result, pagination: meta });
        } else {
          res.status(200).json(result);
        }
      },
      onError = (res, method, error) => res.status(400).json({ error: error.message }),
      methods = ['POST', 'GET', 'PUT', 'PATCH', 'DELETE'] as HttpMethod[],
      hooks = {},
      validate = {},
      softDelete = false,
      select: defaultSelect,
      populate: defaultPopulate,
      searchFields = [],
    } = options;

    // ── CREATE ─ POST /endpoint ─────────────────────────────────────────

    if (methods.includes('POST')) {
      const path = `/${this.endpoint}`;
      this.router.post(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              // Validation hook
              if (validate.create) {
                const validation = validate.create(req.body);
                if (!validation.valid) {
                  return res.status(400).json({ errors: validation.errors });
                }
              }
              // Before hook
              let data = req.body;
              if (hooks.beforeCreate) {
                data = (await hooks.beforeCreate(req, data)) ?? data;
              }
              const result: any = await this.model.create(data);
              // Related model cascade
              if (options.relatedModel && options.relatedMethods?.includes('POST')) {
                await options.relatedModel.create({
                  [options.relatedField!]: result._id,
                  ...data,
                });
              }
              // After hook
              if (hooks.afterCreate) {
                await hooks.afterCreate(req, result);
              }
              onSuccess(res.status(201), 'POST', result);
            } catch (error: any) {
              onError(res, 'POST', error);
            }
          },
          middleware,
          routeMiddleware.create
        )
      );
      this.registerRoute('POST', path);
    }

    // ── BULK CREATE ─ POST /endpoint/bulk ───────────────────────────────

    if (methods.includes('POST')) {
      const path = `/${this.endpoint}/bulk`;
      this.router.post(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              const items = req.body;
              if (!Array.isArray(items)) {
                return res.status(400).json({ error: 'Request body must be an array' });
              }
              // Validate each item
              if (validate.create) {
                for (let i = 0; i < items.length; i++) {
                  const validation = validate.create(items[i]);
                  if (!validation.valid) {
                    return res.status(400).json({
                      error: `Validation failed for item at index ${i}`,
                      errors: validation.errors,
                    });
                  }
                }
              }
              // Before hooks
              let processedItems = items;
              if (hooks.beforeCreate) {
                processedItems = [];
                for (const item of items) {
                  const result = (await hooks.beforeCreate(req, item)) ?? item;
                  processedItems.push(result);
                }
              }
              const results = await this.model.insertMany(processedItems);
              onSuccess(res.status(201), 'POST (Bulk)', results);
            } catch (error: any) {
              onError(res, 'POST (Bulk)', error);
            }
          },
          middleware,
          routeMiddleware.create
        )
      );
      this.registerRoute('POST', path);
    }

    // ── SEARCH ─ GET /endpoint/search ───────────────────────────────────

    if (methods.includes('GET') && searchFields.length > 0) {
      const path = `/${this.endpoint}/search`;
      this.router.get(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              const q = req.query.q as string;
              if (!q) {
                return res.status(400).json({ error: 'Query parameter "q" is required' });
              }
              const fieldsParam = req.query.fields as string | undefined;
              const fields = fieldsParam ? fieldsParam.split(',').map((f) => f.trim()) : searchFields;

              const searchQuery: any = {
                $or: fields.map((field) => ({
                  [field]: { $regex: q, $options: 'i' },
                })),
                ...this.softDeleteFilter(req, softDelete),
              };

              const pageNumber = parseInt(req.query.page as string, 10) || 1;
              const pageSize = parseInt(req.query.limit as string, 10) || 10;
              const skip = (pageNumber - 1) * pageSize;

              const selectStr = this.parseSelect(req.query.select as string, defaultSelect);
              const populateOpt = this.parsePopulate(req.query.populate as string, defaultPopulate);

              let query = this.model.find(searchQuery).skip(skip).limit(pageSize);
              if (selectStr) query = query.select(selectStr);
              if (populateOpt) query = query.populate(populateOpt);

              const [items, total] = await Promise.all([
                query.exec(),
                this.model.countDocuments(searchQuery),
              ]);

              const meta: PaginationMeta = {
                total,
                page: pageNumber,
                limit: pageSize,
                pages: Math.ceil(total / pageSize),
                hasNext: pageNumber * pageSize < total,
                hasPrev: pageNumber > 1,
              };

              let result: any = items;
              if (hooks.afterRead) {
                result = (await hooks.afterRead(req, items as any)) ?? items;
              }
              onSuccess(res, 'GET (Search)', result, meta);
            } catch (error: any) {
              onError(res, 'GET (Search)', error);
            }
          },
          middleware,
          routeMiddleware.read
        )
      );
      this.registerRoute('GET', path, ['q', 'fields', 'page', 'limit', 'select', 'populate']);
    }

    // ── COUNT ─ GET /endpoint/count ─────────────────────────────────────

    if (methods.includes('GET')) {
      const path = `/${this.endpoint}/count`;
      this.router.get(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              const filter = {
                ...this.safeJsonParse(req.query.filter as string),
                ...this.softDeleteFilter(req, softDelete),
              };
              const count = await this.model.countDocuments(filter);
              onSuccess(res, 'GET (Count)', { count });
            } catch (error: any) {
              onError(res, 'GET (Count)', error);
            }
          },
          middleware,
          routeMiddleware.read
        )
      );
      this.registerRoute('GET', path, ['filter']);
    }

    // ── AGGREGATE ─ GET /endpoint/aggregate ─────────────────────────────

    if (methods.includes('GET') && options.aggregatePipeline) {
      const path = `/${this.endpoint}/aggregate`;
      this.router.get(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              const pipeline: any[] =
                typeof options.aggregatePipeline === 'function'
                  ? options.aggregatePipeline(req)
                  : options.aggregatePipeline || [];
              const results = await this.model.aggregate(pipeline);
              onSuccess(res, 'GET (Aggregate)', results);
            } catch (error: any) {
              onError(res, 'GET (Aggregate)', error);
            }
          },
          middleware,
          routeMiddleware.read
        )
      );
      this.registerRoute('GET', path);
    }

    // ── EXISTS ─ GET /endpoint/exists/:id ───────────────────────────────

    if (methods.includes('GET')) {
      const path = `/${this.endpoint}/exists/:id`;
      this.router.get(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              const filter: any = { _id: req.params.id };
              if (softDelete) filter.deletedAt = { $exists: false };
              const exists = await this.model.exists(filter);
              onSuccess(res, 'GET (Exists)', { exists: !!exists });
            } catch (error: any) {
              onError(res, 'GET (Exists)', error);
            }
          },
          middleware,
          routeMiddleware.read
        )
      );
      this.registerRoute('GET', path, ['id']);
    }

    // ── READ ALL ─ GET /endpoint ────────────────────────────────────────

    if (methods.includes('GET')) {
      const path = `/${this.endpoint}`;
      this.router.get(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              let filter = {
                ...this.safeJsonParse(req.query.filter as string),
                ...this.softDeleteFilter(req, softDelete),
              };
              const sortOrder = this.safeJsonParse(req.query.sort as string);
              const pageNumber = parseInt(req.query.page as string, 10) || 1;
              const pageSize = parseInt(req.query.limit as string, 10) || 10;
              const skip = (pageNumber - 1) * pageSize;
              const selectStr = this.parseSelect(req.query.select as string, defaultSelect);
              const populateOpt = this.parsePopulate(req.query.populate as string, defaultPopulate);

              // Before read hook
              if (hooks.beforeRead) {
                filter = (await hooks.beforeRead(req, filter)) ?? filter;
              }

              let items: T[];
              if (options.relatedModel && options.relatedMethods?.includes('GET')) {
                items = await this.model.aggregate([
                  { $match: filter },
                  {
                    $lookup: {
                      from: options.relatedModel.collection.name,
                      localField: options.relatedField!,
                      foreignField: '_id',
                      as: 'relatedData',
                    },
                  },
                  { $sort: Object.keys(sortOrder).length ? sortOrder : { _id: -1 } },
                  { $skip: skip },
                  { $limit: pageSize },
                ]);
              } else {
                let query = this.model
                  .find(filter)
                  .sort(Object.keys(sortOrder).length ? sortOrder : undefined)
                  .skip(skip)
                  .limit(pageSize);
                if (selectStr) query = query.select(selectStr);
                if (populateOpt) query = query.populate(populateOpt);
                items = await query.exec();
              }

              const total = await this.model.countDocuments(filter);
              const meta: PaginationMeta = {
                total,
                page: pageNumber,
                limit: pageSize,
                pages: Math.ceil(total / pageSize),
                hasNext: pageNumber * pageSize < total,
                hasPrev: pageNumber > 1,
              };

              let result: T | T[] = items;
              if (hooks.afterRead) {
                result = (await hooks.afterRead(req, items)) ?? items;
              }
              onSuccess(res, 'GET', result, meta);
            } catch (error: any) {
              onError(res, 'GET', error);
            }
          },
          middleware,
          routeMiddleware.read
        )
      );
      this.registerRoute('GET', path, ['filter', 'sort', 'page', 'limit', 'select', 'populate']);
    }

    // ── READ ONE ─ GET /endpoint/:id ────────────────────────────────────

    if (methods.includes('GET')) {
      const path = `/${this.endpoint}/:id`;
      this.router.get(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              const selectStr = this.parseSelect(req.query.select as string, defaultSelect);
              const populateOpt = this.parsePopulate(req.query.populate as string, defaultPopulate);

              let item: T | null;
              if (options.relatedModel && options.relatedMethods?.includes('GET')) {
                const matchFilter: any = { _id: new Types.ObjectId(req.params.id) };
                if (softDelete) matchFilter.deletedAt = { $exists: false };
                const aggregateResult = await this.model.aggregate([
                  { $match: matchFilter },
                  {
                    $lookup: {
                      from: options.relatedModel.collection.name,
                      localField: options.relatedField!,
                      foreignField: '_id',
                      as: 'relatedData',
                    },
                  },
                ]);
                item = (aggregateResult[0] as T) || null;
              } else {
                const findFilter: any = { _id: req.params.id };
                if (softDelete) findFilter.deletedAt = { $exists: false };
                let query = this.model.findOne(findFilter);
                if (selectStr) query = query.select(selectStr);
                if (populateOpt) query = query.populate(populateOpt);
                item = await query.exec();
              }

              if (!item) {
                return res.status(404).json({ message: 'Item not found' });
              }

              let result: T | T[] = item;
              if (hooks.afterRead) {
                result = (await hooks.afterRead(req, item)) ?? item;
              }
              onSuccess(res, 'GET', result);
            } catch (error: any) {
              onError(res, 'GET', error);
            }
          },
          middleware,
          routeMiddleware.read
        )
      );
      this.registerRoute('GET', path, ['id', 'select', 'populate']);
    }

    // ── UPDATE (FULL) ─ PUT /endpoint/:id ───────────────────────────────

    if (methods.includes('PUT')) {
      const path = `/${this.endpoint}/:id`;
      this.router.put(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              if (validate.update) {
                const validation = validate.update(req.body);
                if (!validation.valid) {
                  return res.status(400).json({ errors: validation.errors });
                }
              }
              let data = req.body;
              if (hooks.beforeUpdate) {
                data = (await hooks.beforeUpdate(req, req.params.id, data)) ?? data;
              }
              const item = await this.model.findByIdAndUpdate(req.params.id, data, {
                new: true,
                runValidators: true,
              });
              if (!item) {
                return res.status(404).json({ message: 'Item not found' });
              }
              if (options.relatedModel && options.relatedMethods?.includes('PUT')) {
                await options.relatedModel.updateMany(
                  { [options.relatedField!]: item._id },
                  data
                );
              }
              if (hooks.afterUpdate) {
                await hooks.afterUpdate(req, item);
              }
              onSuccess(res, 'PUT', item);
            } catch (error: any) {
              onError(res, 'PUT', error);
            }
          },
          middleware,
          routeMiddleware.update
        )
      );
      this.registerRoute('PUT', path, ['id']);
    }

    // ── UPDATE (PARTIAL) ─ PATCH /endpoint/:id ──────────────────────────

    if (methods.includes('PATCH')) {
      const path = `/${this.endpoint}/:id`;
      this.router.patch(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              if (validate.update) {
                const validation = validate.update(req.body);
                if (!validation.valid) {
                  return res.status(400).json({ errors: validation.errors });
                }
              }
              let data = req.body;
              if (hooks.beforeUpdate) {
                data = (await hooks.beforeUpdate(req, req.params.id, data)) ?? data;
              }
              const item = await this.model.findByIdAndUpdate(
                req.params.id,
                { $set: data },
                { new: true, runValidators: true }
              );
              if (!item) {
                return res.status(404).json({ message: 'Item not found' });
              }
              if (hooks.afterUpdate) {
                await hooks.afterUpdate(req, item);
              }
              onSuccess(res, 'PATCH', item);
            } catch (error: any) {
              onError(res, 'PATCH', error);
            }
          },
          middleware,
          routeMiddleware.update
        )
      );
      this.registerRoute('PATCH', path, ['id']);
    }

    // ── BULK UPDATE ─ PATCH /endpoint/bulk ──────────────────────────────

    if (methods.includes('PATCH')) {
      const path = `/${this.endpoint}/bulk`;
      this.router.patch(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              const { filter, update } = req.body;
              if (!filter || !update) {
                return res.status(400).json({
                  error: 'Request body must contain "filter" and "update" objects',
                });
              }
              if (validate.update) {
                const validation = validate.update(update);
                if (!validation.valid) {
                  return res.status(400).json({ errors: validation.errors });
                }
              }
              const result = await this.model.updateMany(filter, { $set: update }, { runValidators: true });
              onSuccess(res, 'PATCH (Bulk)', result);
            } catch (error: any) {
              onError(res, 'PATCH (Bulk)', error);
            }
          },
          middleware,
          routeMiddleware.update
        )
      );
      this.registerRoute('PATCH', path, ['filter', 'update']);
    }

    // ── SOFT DELETE RESTORE ─ PATCH /endpoint/:id/restore ───────────────

    if (softDelete && (methods.includes('PATCH') || methods.includes('PUT'))) {
      const path = `/${this.endpoint}/:id/restore`;
      this.router.patch(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              const item = await this.model.findByIdAndUpdate(
                req.params.id,
                { $unset: { deletedAt: 1 } },
                { new: true }
              );
              if (!item) {
                return res.status(404).json({ message: 'Item not found' });
              }
              onSuccess(res, 'PATCH (Restore)', item);
            } catch (error: any) {
              onError(res, 'PATCH (Restore)', error);
            }
          },
          middleware,
          routeMiddleware.update
        )
      );
      this.registerRoute('PATCH', path, ['id']);
    }

    // ── DELETE MULTIPLE ─ DELETE /endpoint ───────────────────────────────

    if (methods.includes('DELETE')) {
      const path = `/${this.endpoint}`;
      this.router.delete(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              const query = this.safeJsonParse(req.query.filter as string);
              if (Object.keys(query).length === 0) {
                return res.status(400).json({
                  error: 'A filter is required for bulk delete to prevent accidental data loss',
                });
              }
              let deleteResult: any;
              if (softDelete) {
                deleteResult = await this.model.updateMany(query, {
                  $set: { deletedAt: new Date() },
                });
              } else {
                deleteResult = await this.model.deleteMany(query);
              }
              if ((deleteResult.deletedCount ?? deleteResult.modifiedCount) === 0) {
                return res.status(404).json({ message: 'No matching items found to delete' });
              }
              if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
                const ids = (await this.model.find(query).select('_id')).map((d: any) => d._id);
                await options.relatedModel.deleteMany({
                  [options.relatedField!]: { $in: ids },
                });
              }
              onSuccess(res, 'DELETE', deleteResult);
            } catch (error: any) {
              onError(res, 'DELETE', error);
            }
          },
          middleware,
          routeMiddleware.delete
        )
      );
      this.registerRoute('DELETE', path, ['filter']);
    }

    // ── DELETE ONE ─ DELETE /endpoint/:id ────────────────────────────────

    if (methods.includes('DELETE')) {
      const path = `/${this.endpoint}/:id`;
      this.router.delete(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              if (hooks.beforeDelete) {
                await hooks.beforeDelete(req, req.params.id);
              }
              let item: T | null;
              if (softDelete) {
                item = await this.model.findByIdAndUpdate(
                  req.params.id,
                  { $set: { deletedAt: new Date() } as any },
                  { new: true }
                );
              } else {
                item = await this.model.findByIdAndDelete(req.params.id);
              }
              if (!item) {
                return res.status(404).json({ message: 'Item not found' });
              }
              if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
                await options.relatedModel.deleteMany({
                  [options.relatedField!]: item._id,
                });
              }
              if (hooks.afterDelete) {
                await hooks.afterDelete(req, item);
              }
              onSuccess(res, 'DELETE', item);
            } catch (error: any) {
              onError(res, 'DELETE', error);
            }
          },
          middleware,
          routeMiddleware.delete
        )
      );
      this.registerRoute('DELETE', path, ['id']);
    }

    // ── BULK DELETE ─ DELETE /endpoint/bulk ──────────────────────────────

    if (methods.includes('DELETE')) {
      const path = `/${this.endpoint}/bulk`;
      this.router.delete(
        path,
        ...this.buildMiddlewareChain(
          async (req: Request, res: Response) => {
            try {
              const { ids } = req.body;
              if (!Array.isArray(ids) || ids.length === 0) {
                return res.status(400).json({
                  error: 'Request body must contain a non-empty "ids" array',
                });
              }
              let deleteResult: any;
              if (softDelete) {
                deleteResult = await this.model.updateMany(
                  { _id: { $in: ids } },
                  { $set: { deletedAt: new Date() } }
                );
              } else {
                deleteResult = await this.model.deleteMany({ _id: { $in: ids } });
              }
              onSuccess(res, 'DELETE (Bulk)', deleteResult);
            } catch (error: any) {
              onError(res, 'DELETE (Bulk)', error);
            }
          },
          middleware,
          routeMiddleware.delete
        )
      );
      this.registerRoute('DELETE', path);
    }

    // ── CUSTOM ROUTES ───────────────────────────────────────────────────

    if (options.customRoutes) {
      options.customRoutes.forEach((route) => {
        const { method, path, handler, middleware: routeMw } = route;
        const fullPath = `/${this.endpoint}${path}`;
        this.router[method](
          fullPath,
          ...this.buildMiddlewareChain(handler, middleware, routeMw)
        );
        this.registerRoute(method.toUpperCase(), fullPath);
      });
    }
  }

  // ── Public API ──────────────────────────────────────────────────────────

  /** Returns the configured Express Router with all CRUD routes. */
  public getRouter(): Router {
    return this.router;
  }

  /** Returns an array of all registered route definitions. */
  public getRoutes(): RouteInfo[] {
    return this.routes;
  }
}

export { CrudController };
export default CrudController;
