import { Request, Response, Router, NextFunction } from 'express';
import { Document, Model, Aggregate } from 'mongoose';

/**
 * Options for configuring the CRUD controller behavior.
 */
export interface CrudOptions<T extends Document> {
  middleware?: ((req: Request, res: Response, next: NextFunction) => void)[];
  onSuccess?: (res: Response, method: string, result: T | T[] | any) => void;
  onError?: (res: Response, method: string, error: Error) => void;
  methods?: ('POST' | 'GET' | 'PUT' | 'DELETE')[];
  relatedModel?: Model<any>;
  relatedField?: string;
  relatedMethods?: ('POST' | 'GET' | 'PUT' | 'DELETE')[];
  aggregatePipeline?: object[];
  customRoutes?: {
    method: 'post' | 'get' | 'put' | 'delete',
    path: string,
    handler: (req: Request, res: Response) => void
  }[];
}

/**
 * A generic CRUD Controller for Express/Mongoose API development.
 * 
 * It registers standard CRUD endpoints based on the provided model and endpoint string,
 * along with any custom routes.
 */
class CrudController<T extends Document> {
  private model: Model<T>;
  private endpoint: string;
  private router: Router;
  private routes: { method: string; path: string; params?: string[] }[];

  constructor(model: Model<T>, endpoint: string, options: CrudOptions<T> = {}) {
    this.model = model;
    this.endpoint = endpoint;
    this.router = Router();
    this.routes = [];
    this.configureRoutes(options);
  }

  /**
   * Applies middleware to the route handler.
   * @param routeHandler The original route handler.
   * @param middlewareList Optional array of middleware functions.
   * @returns Array of middleware functions including the route handler.
   */
 private applyMiddleware(handler: (req: Request, res: Response) => void, middleware: ((req: Request, res: Response, next: NextFunction) => void)[]) {
  return [...middleware, handler];
}


  /**
   * Registers route definitions to the internal list.
   * @param method HTTP method.
   * @param path URL path.
   * @param params Optional route parameter names.
   */
  private registerRoute(method: string, path: string, params?: string[]) {
    this.routes.push({ method, path, params });
  }

  /**
   * Configures all endpoints based on given options.
   * @param options CrudOptions to setup CRUD behaviors.
   */
  private configureRoutes(options: CrudOptions<T>) {
    // Destructure and set default middleware and callbacks
    const {
      middleware = [],
      onSuccess = (res, method, result) => res.status(200).send(result),
      onError = (res, method, error) => res.status(400).send(error),
      methods = ['POST', 'GET', 'PUT', 'DELETE']
    } = options;

    // CREATE operation - POST /endpoint
    if (methods.includes('POST')) {
      const path = `/${this.endpoint}`;
      this.router.post(
        path,
        this.applyMiddleware(async (req, res) => {
          const method = 'POST';
          try {
            const result: any = await this.model.create(req.body);
            // If a related model is defined and supports POST, create related entry
            if (options.relatedModel && options.relatedMethods?.includes('POST')) {
              await options.relatedModel.create({
                [options.relatedField!]: result._id,
                ...req.body
              });
            }
            // Return 201 Created status if successful
            onSuccess(res.status(201), method, result);
          } catch (error: any) {
            onError(res, method, error);
          }
        }, middleware)
      );
      this.registerRoute('POST', path);
    }

    // READ ALL operation - GET /endpoint
    if (methods.includes('GET')) {
      const path = `/${this.endpoint}`;
      this.router.get(
        path,
        this.applyMiddleware(async (req, res) => {
          const method = 'GET';
          try {
            const { filter, sort, page, limit } = req.query;
            const query = filter ? JSON.parse(filter as string) : {};
            const sortOrder = sort ? JSON.parse(sort as string) : {};
            const pageNumber = parseInt(page as string, 10) || 1;
            const pageSize = parseInt(limit as string, 10) || 10;
            const skip = (pageNumber - 1) * pageSize;

            let items: T[] | Aggregate<any[]>;
            if (options.relatedModel && options.relatedMethods?.includes('GET')) {
              // Use aggregation with lookup if related model exists
              items = await this.model.aggregate([
                { $match: query },
                {
                  $lookup: {
                    from: options.relatedModel.collection.name,
                    localField: options.relatedField!,
                    foreignField: '_id',
                    as: 'relatedData'
                  }
                },
                { $sort: sortOrder },
                { $skip: skip },
                { $limit: pageSize }
              ]);
            } else {
              items = await this.model.find(query).sort(sortOrder).skip(skip).limit(pageSize);
            }
            onSuccess(res, method, items);
          } catch (error: any) {
            onError(res, method, error);
          }
        }, middleware)
      );
      this.registerRoute('GET', path, ['filter', 'sort', 'page', 'limit']);
    }

    // READ ONE operation - GET /endpoint/:id
    if (methods.includes('GET')) {
      const path = `/${this.endpoint}/:id`;
      this.router.get(
        path,
        this.applyMiddleware(async (req, res) => {
          const method = 'GET';
          try {
            let item: T | null;
            if (options.relatedModel && options.relatedMethods?.includes('GET')) {
              const aggregateResult = await this.model.aggregate([
                { $match: { _id: req.params.id } },
                {
                  $lookup: {
                    from: options.relatedModel.collection.name,
                    localField: options.relatedField!,
                    foreignField: '_id',
                    as: 'relatedData'
                  }
                }
              ]);
              item = aggregateResult[0] as T || null;
            } else {
              item = await this.model.findById(req.params.id);
            }
            if (!item) {
              return res.status(404).send({ message: 'Item not found' });
            }
            onSuccess(res, method, item);
          } catch (error: any) {
            onError(res, method, error);
          }
        }, middleware)
      );
      this.registerRoute('GET', path, ['id']);
    }

    // UPDATE operation - PUT /endpoint/:id
    if (methods.includes('PUT')) {
      const path = `/${this.endpoint}/:id`;
      this.router.put(
        path,
        this.applyMiddleware(async (req, res) => {
          const method = 'PUT';
          try {
            const item = await this.model.findByIdAndUpdate(req.params.id, req.body, { new: true, runValidators: true });
            if (!item) {
              return res.status(404).send({ message: 'Item not found' });
            }
            // Update related model entries if configured
            if (options.relatedModel && options.relatedMethods?.includes('PUT')) {
              await options.relatedModel.updateMany({ [options.relatedField!]: item._id }, req.body);
            }
            onSuccess(res, method, item);
          } catch (error: any) {
            onError(res, method, error);
          }
        }, middleware)
      );
      this.registerRoute('PUT', path, ['id']);
    }

    // DELETE MULTIPLE operation - DELETE /endpoint?filter=...
    if (methods.includes('DELETE')) {
      const path = `/${this.endpoint}`;
      this.router.delete(
        path,
        this.applyMiddleware(async (req, res) => {
          const method = 'DELETE';
          try {
            const query = req.query.filter ? JSON.parse(req.query.filter as string) : {};
            const deleteResult: any = await this.model.deleteMany(query);
            if (deleteResult.deletedCount === 0) {
              return res.status(404).send({ message: 'No matching items found to delete' });
            }
            if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
              await options.relatedModel.deleteMany({ [options.relatedField!]: { $in: query } });
            }
            onSuccess(res, method, deleteResult);
          } catch (error: any) {
            onError(res, method, error);
          }
        }, middleware)
      );
      this.registerRoute('DELETE', path, ['filter']);
    }

    // DELETE ONE operation - DELETE /endpoint/:id
    if (methods.includes('DELETE')) {
      const path = `/${this.endpoint}/:id`;
      this.router.delete(
        path,
        this.applyMiddleware(async (req, res) => {
          const method = 'DELETE';
          try {
            const item = await this.model.findByIdAndDelete(req.params.id);
            if (!item) {
              return res.status(404).send({ message: 'Item not found' });
            }
            if (options.relatedModel && options.relatedMethods?.includes('DELETE')) {
              await options.relatedModel.deleteMany({ [options.relatedField!]: item._id });
            }
            onSuccess(res, method, item);
          } catch (error: any) {
            onError(res, method, error);
          }
        }, middleware)
      );
      this.registerRoute('DELETE', path, ['id']);
    }

    // AGGREGATE operation - GET /endpoint/aggregate
    if (methods.includes('GET') && options.aggregatePipeline) {
      const path = `/${this.endpoint}/aggregate`;
      this.router.get(
        path,
        this.applyMiddleware(async (req, res) => {
          const method = 'GET (Aggregate)';
          try {
            const pipeline: any[] = options.aggregatePipeline || [];
            const results = await this.model.aggregate(pipeline);
            onSuccess(res, method, results);
          } catch (error: any) {
            onError(res, method, error);
          }
        }, middleware)
      );
      this.registerRoute('GET', path);
    }

    // CUSTOM ROUTES
    if (options.customRoutes) {
      options.customRoutes.forEach(route => {
        const { method, path, handler } = route;
        if (methods.includes(method.toUpperCase() as 'POST' | 'GET' | 'PUT' | 'DELETE')) {
          // Prepend base endpoint to custom route path
          this.router[method](`/${this.endpoint}${path}`, this.applyMiddleware(handler, middleware));
          this.registerRoute(method.toUpperCase(), `/${this.endpoint}${path}`);
        }
      });
    }
  }

  /**
   * Returns the configured Express router.
   */
  public getRouter(): Router {
    return this.router;
  }

  /**
   * Returns an array of registered route definitions.
   */
  public getRoutes(): { method: string; path: string; params?: string[] }[] {
    return this.routes;
  }
}

export default CrudController;
