import {
  ObjectId,
} from 'bson';
import {
  NextFunction,
  Response,
} from 'express';
import {
  Types,
} from 'mongoose';

import {
  isString,
} from './helpers/isString';
import {
  IApiError,
} from './types/IApiError';
import {
  ApiDocument,
  ApiModel,
} from './types/IApiModel';
import {
  IApiRequest,
} from './types/IApiRequest';

abstract class ApiController<T extends ApiDocument> {
  protected model: ApiModel<T>;

  constructor(model: ApiModel<T>) {
    this.model = model;
  }

  // ERROR Responses
  public respondServerError(res: Response, error: any): Response {
    return res.status(500).json({ error });
  }
  public respondNotFound(
    id: string | number | ObjectId,
    res: Response,
    modelName: string,
  ): Response {
    const error: IApiError = {
      id: 'notFound',
      message: `${ modelName } ${ id } does not exist`,
      fields: {},
      errors: [],
    };

    return res.status(404).json({
      error,
    });
  }
  public respondInvalidId(res: Response): Response {
    const error: IApiError = {
      id: 'invalidId',
      message: 'Invalid id',
      fields: {},
      errors: [],
    };

    return res.status(400).json({
      error,
    });
  }
  public respondModelMissingError(res: Response): Response {
    const error: IApiError = {
      id: 'modelMissing',
      message: 'the model is missing in the request',
      fields: {},
      errors: [],
    };

    return res.status(400).json({
      error,
    });
  }
  public respondDeletionError(res: Response, err: Error): Response {
    const error: IApiError = {
      id: 'delete',
      message: err.message,
      fields: {},
      errors: [],
    };

    return res.status(400).json({
      error,
    });
  }
  public respondValidationError(err: any, res: Response, next: NextFunction): Response | void {
    if (err.name === 'ValidationError') {
      const error: IApiError = {
        id: 'validationError',
        message: err.message,
        fields: err.errors,
        errors: [],
      };

      return res.status(400).json({
        error,
      });
    }
    if (err.code === 11000) {
      return res.status(400).json({
        error: {
          id: 'duplicate',
          message: `${ this.model.modelName } already exists`,
        },
      });
    } else {
      return next(err);
    }
  }
  public apiResponse(req: IApiRequest, res: Response, _next: NextFunction): Response {
    const hasMetaError = req.meta && req.meta.error;
    let status = 200;
    if (hasMetaError) {
      status = 500;
    }

    return res.status(status).json({ meta: req.meta, data: req.data });
  }
  public async populateMeta(req: IApiRequest, _res: Response, next: NextFunction): Promise<void> {
    const qTotal = req.modelQuery?.total || {};

    return this.model.countDocuments(qTotal)
      .then((total: number) => {
        const offset = req.modelQuery?.offset || 0;
        const limit = req.modelQuery?.limit || 100;
        req.meta = {
          total: total,
          count: req.data ? req.data.length : 0,
          offset: isString(offset) ? parseInt(offset, 10) : offset,
          limit: isString(limit) ? parseInt(limit, 10) : limit,
        };

        return next();
      })
      .catch((err: unknown) => next(err));
  }
  protected hasModel<K extends ApiDocument>(model: ApiDocument | null | undefined): model is K {
    return typeof model !== 'undefined'
    && model !== null
    && model.constructor?.name === 'model'
    && Types.ObjectId.isValid(model._id);
  }
}

export default ApiController;
