/**
 * Copyright (c) 2023-present Plane Software, Inc. and contributors
 * SPDX-License-Identifier: AGPL-3.0-only
 * See the LICENSE file for details.
 */

import type { RequestHandler, Router, Request } from "express";
import type { WebSocket } from "ws";

import "reflect-metadata";

export type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "ws";

type ControllerInstance = {
  [key: string]: any;
};

export type ControllerConstructor = {
  new (...args: any[]): ControllerInstance;
  prototype: ControllerInstance;
};

export function registerController(
  router: Router,
  Controller: ControllerConstructor,
  dependencies: unknown[] = []
): void {
  // Create the controller instance with dependencies
  const instance = new Controller(...dependencies);

  // Determine if it's a WebSocket controller or REST controller by checking
  // if it has any methods with the "ws" method metadata
  const isWebsocket = Object.getOwnPropertyNames(Controller.prototype).some((methodName) => {
    if (methodName === "constructor") return false;
    return Reflect.getMetadata("method", instance, methodName) === "ws";
  });

  if (isWebsocket) {
    // Register as WebSocket controller
    // Pass the existing instance with dependencies to avoid creating a new instance without them
    registerWebSocketController(router, Controller, instance);
  } else {
    // Register as REST controller with the existing instance
    registerRestController(router, Controller, instance);
  }
}

function registerRestController(
  router: Router,
  Controller: ControllerConstructor,
  existingInstance?: ControllerInstance
): void {
  const instance = existingInstance || new Controller();
  const baseRoute = Reflect.getMetadata("baseRoute", Controller) as string;

  Object.getOwnPropertyNames(Controller.prototype).forEach((methodName) => {
    if (methodName === "constructor") return; // Skip the constructor

    const method = Reflect.getMetadata("method", instance, methodName) as HttpMethod;
    const route = Reflect.getMetadata("route", instance, methodName) as string;
    const middlewares = (Reflect.getMetadata("middlewares", instance, methodName) as RequestHandler[]) || [];

    if (method && route) {
      const handler = instance[methodName] as unknown;

      if (typeof handler === "function") {
        if (method !== "ws") {
          (router[method] as (path: string, ...handlers: RequestHandler[]) => void)(
            `${baseRoute}${route}`,
            ...middlewares,
            handler.bind(instance)
          );
        }
      }
    }
  });
}

function registerWebSocketController(
  router: Router,
  Controller: ControllerConstructor,
  existingInstance?: ControllerInstance
): void {
  const instance = existingInstance || new Controller();
  const baseRoute = Reflect.getMetadata("baseRoute", Controller) as string;

  Object.getOwnPropertyNames(Controller.prototype).forEach((methodName) => {
    if (methodName === "constructor") return; // Skip the constructor

    const method = Reflect.getMetadata("method", instance, methodName) as string;
    const route = Reflect.getMetadata("route", instance, methodName) as string;

    if (method === "ws" && route) {
      const handler = instance[methodName] as unknown;

      if (typeof handler === "function" && "ws" in router && typeof router.ws === "function") {
        router.ws(`${baseRoute}${route}`, (ws: WebSocket, req: Request) => {
          try {
            handler.call(instance, ws, req);
          } catch (error) {
            console.error(`WebSocket error in ${Controller.name}.${methodName}`, error);
            ws.close(1011, error instanceof Error ? error.message : "Internal server error");
          }
        });
      }
    }
  });
}
