/* eslint-disable @typescript-eslint/no-explicit-any,max-params */
import * as Boom from "@hapi/boom";
import * as Hapi from "@hapi/hapi";
import { CrudRepository } from "../records/crud-repository.js";
import { createResponse } from "../helpers/response.js";
import {
  PgRecordInfo,
  UnsavedR,
  SavedR,
  KeyType,
} from "../records/record-info.js";

/**
 *
 * A generic controller that handles exposes a [[CrudRepository]] as HTTP
 * endpoints with full POST, PUT, GET, DELETE semantics.
 *
 * The controller yanks the instance of the crud repository out of the request at runtime.
 * This assumes you're following the pattern exposed with the hapi-pg-promise plugin.
 *
 * @typeParam ID The type of the id column
 * @typeParam T The type of the record
 */
export abstract class AbstractCrudController<
  TUnsavedR,
  TSavedR extends TUnsavedR & IdKeyT,
  IdKeyT extends object
> {
  /**
   * @param repoName the key at which the repository for the record can be accessed (that is, request.db[repoName])
   * @param paramsIdField the placeholder used in the Hapi route for the id of the record
   * @param dbDecoration the decorated function on the request to use (defaults to request.db())
   */

  abstract repoName: string;
  abstract paramsIdField;
  abstract dbDecoration;
  abstract recordType: PgRecordInfo<TUnsavedR, TSavedR, IdKeyT>;

  repo(request: Hapi.Request): CrudRepository<TUnsavedR, TSavedR, IdKeyT> {
    const db = request[this.dbDecoration];
    if (!db)
      throw Boom.badImplementation(
        `CrudController for table ${this.recordType.tableName} could not find request decoration '${this.dbDecoration}'`
      );
    const repo = db()[this.repoName];
    if (!repo)
      throw Boom.badImplementation(
        `CrudController for table ${this.recordType.tableName} could not find repository for '${this.dbDecoration}().${this.repoName}'`
      );
    return repo;
  }

  /**
   * Creates a new record
   */
  public create = async (
    request: Hapi.Request,
    toolkit: Hapi.ResponseToolkit
  ): Promise<any> => {
    try {
      // would love to know how to get rid of this double cast hack
      const payload: TSavedR = <TSavedR>(<any>request.payload);
      const data: TSavedR = await this.repo(request).insert(payload);

      return toolkit.response(
        createResponse(request, {
          value: data,
        })
      );
    } catch (error: any) {
      return toolkit.response(
        createResponse(request, {
          boom: Boom.badImplementation(error),
        })
      );
    }
  };

  /**
   * Updates a record by ID. This method can accept partial updates.
   */
  public updateById = async (
    request: Hapi.Request,
    toolkit: Hapi.ResponseToolkit
  ): Promise<any> => {
    try {
      const payload: Partial<TSavedR> = <any>request.payload;
      const id: IdKeyT = request.params[this.paramsIdField];
      const updatedRow: TSavedR = await this.repo(request).updateById(
        id,
        payload
      );

      if (!updatedRow) {
        return toolkit.response(
          createResponse(request, {
            boom: Boom.notFound(),
          })
        );
      }

      return toolkit.response(
        createResponse(request, {
          value: updatedRow,
        })
      );
    } catch (error: any) {
      return toolkit.response(
        createResponse(request, {
          boom: Boom.badImplementation(error),
        })
      );
    }
  };

  /**
   * Return a record given its id.
   */
  public getById = async (
    request: Hapi.Request,
    toolkit: Hapi.ResponseToolkit
  ): Promise<any> => {
    try {
      const id: IdKeyT = request.params[this.paramsIdField];
      const row: TSavedR = await this.repo(request).findById(id);

      if (!row) {
        return toolkit.response(
          createResponse(request, {
            boom: Boom.notFound(),
          })
        );
      }

      return toolkit.response(
        createResponse(request, {
          value: row,
        })
      );
    } catch (error: any) {
      return toolkit.response(
        createResponse(request, {
          boom: Boom.badImplementation(error),
        })
      );
    }
  };

  /**
   * Return all records.
   */
  public getAll = async (
    request: Hapi.Request,
    toolkit: Hapi.ResponseToolkit
  ): Promise<any> => {
    try {
      const rows: TSavedR[] = await this.repo(request).findAll();

      return toolkit.response(
        createResponse(request, {
          value: rows,
        })
      );
    } catch (error: any) {
      return toolkit.response(
        createResponse(request, {
          boom: Boom.badImplementation(error),
        })
      );
    }
  };

  /**
   * Delete a record given its id.
   */
  public deleteById = async (
    request: Hapi.Request,
    toolkit: Hapi.ResponseToolkit
  ): Promise<any> => {
    try {
      const id: IdKeyT = request.params[this.paramsIdField];

      const count = await this.repo(request).removeById(id);

      if (count === 0) {
        return createResponse(request, { boom: Boom.notFound() });
      }

      return toolkit.response(
        createResponse(request, {
          value: { id },
        })
      );
    } catch (error: any) {
      return toolkit.response(
        createResponse(request, {
          boom: Boom.badImplementation(error),
        })
      );
    }
  };
}

export function unboundCrudController<TRecordInfo extends PgRecordInfo>(
  aRecordType: TRecordInfo
) {
  return class CrudController extends AbstractCrudController<
    UnsavedR<TRecordInfo>,
    SavedR<TRecordInfo>,
    KeyType<TRecordInfo>
  > {
    public readonly repoName: string;
    public readonly paramsIdField;
    public readonly dbDecoration;
    public readonly recordType = aRecordType;

    constructor(repoName: string, paramsIdField = "id", dbDecoration = "db") {
      super();
      this.repoName = repoName;
      this.paramsIdField = paramsIdField;
      this.dbDecoration = dbDecoration;
    }
  };
}

export function CrudControllerBase<Rec extends PgRecordInfo>(recordType: Rec) {
  return unboundCrudController<Rec>(recordType);
}

export const crudRoutesFor = (
  name: string,
  path: string,
  controller: AbstractCrudController<any, any, any>,
  idParam: string,
  validate: Record<string, Hapi.RouteOptionsValidate>
): Hapi.ServerRoute[] => [
  {
    method: "POST",
    path: `${path}`,
    options: {
      handler: controller.create,
      validate: validate.create,
      description: `Method that creates a new ${name}.`,
      tags: ["api", name],
    },
  },
  {
    method: "PUT",
    path: `${path}/{${idParam}}`,
    options: {
      handler: controller.updateById,
      validate: validate.updateById,
      description: `Method that updates a ${name} by its id.`,
      tags: ["api", name],
    },
  },
  {
    method: "GET",
    path: `${path}/{${idParam}}`,
    options: {
      handler: controller.getById,
      validate: validate.getById,
      description: `Method that gets a ${name} by its id.`,
      tags: ["api", name],
    },
  },
  {
    method: "GET",
    path: `${path}`,
    options: {
      handler: controller.getAll,
      description: `Method that gets all ${name}s.`,
      tags: ["api", name],
    },
  },
  {
    method: "DELETE",
    path: `${path}/{${idParam}}`,
    options: {
      handler: controller.deleteById,
      validate: validate.deleteById,
      description: `Method that deletes a ${name} by its id.`,
      tags: ["api", name],
    },
  },
];
