import type { FastifyInstance } from "fastify"
import type { Schema, Model, Document } from "mongoose"
import type {
  FrontendPayload,
  FrontendPayloadRequestData,
  HttpService,
  PluginSchemaDefinition,
  IntervalTaskOptions,
} from "@/types"
import type { CrudHandlers } from "@c/crud/crudTypes"
import type {
  PluginFastifyInstance,
  PluginOptions,
  RegisterForFrontendGenerationInput,
} from "./pluginTypes"
import mongoose from "mongoose"
import type EventEmitter from "@c/events/eventEmitter"
import { type Listener } from "@c/events/eventEmitter"
import { createCacheService } from "@m/cache/cacheManager"
import { createPayload } from "@c/payload/payloadRegistrar"
import { registerCrudHandlers } from "@c/crud/crudRegistrar"
import { registerPayloadHandler } from "@c/payload/payloadRegistrar"
import { registerPluginForFrontendGeneration } from "@c/pluginSystem/configGenerator"
import { registerSchemaDefinition } from "@c/validation/schemaGenerator"
import type { PluginContract } from "@c/contracting/contractTypes"
import { registerPluginContract } from "@c/contracting/contractRegistrar"
import configService from "@c/config/configService"

const EVENT_WHITELIST = [
  "broadcastDataUpdated",
  "broadcastDataCreated",
  "broadcastDataDeleted",
]

export const createPluginFastifyInstance = (
  fastify: FastifyInstance,
  pluginOptions: PluginOptions,
): PluginFastifyInstance => {
  // here we add everything that we want to pass from fastify to the plugins
  // + some additionally encapsulated functions

  // encapsulate the plugin log to it's own child logger
  const pluginLogger = fastify.log.child({
    plugin: pluginOptions.key,
    name: pluginOptions.key,
  })

  const pluginCacheService = createCacheService(fastify.cache, {
    prefix: pluginOptions.key,
    // Default 60 minutes TTL for plugins -> TODO: should be a raclette.config option
    // do we want this per-plugin? probably not, global is ok
    defaultTtl: configService.getConfig()?.backend?.cacheTTL ?? -1,
  })

  const instance: PluginFastifyInstance = {
    register: (() => {}) as any,
    get: fastify.get,
    post: fastify.post,
    put: fastify.put,
    patch: fastify.patch,
    delete: fastify.delete,
    authenticate: fastify.authenticate,
    authenticatedOrWorksession: () =>
      fastify.authenticatedOrWorksession(pluginOptions.key),
    checkPermissions: (permissionsArr) =>
      fastify.checkPermissions(permissionsArr),

    // Plugin-scoped logger with plugin-specific prefix
    log: pluginLogger,

    // Enforce prefixed model and collection naming
    createModel: <T extends Document>(
      modelName: string,
      schema: Schema,
    ): Model<T> => {
      const prefixedModelName = `${pluginOptions.collectionPrefix}${modelName}`
      const collectionName = `${pluginOptions.collectionPrefix}${modelName.toLowerCase()}`

      return mongoose.model<T>(prefixedModelName, schema, collectionName)
    },

    registerSocket: (eventName: string, handler: Listener) => {
      const prefixedEventName = `${pluginOptions.key}--${eventName}`

      // Defer socket and eventbus registration until app is ready
      fastify.ready().then(() => {
        if (!fastify.eventbus) {
          throw new Error(
            "Eventbus is not available. Ensure it is registered before plugins.",
          )
        }

        // Register with the eventbus
        fastify.eventbus.on(prefixedEventName, handler)
        fastify.log.info(
          `[registerSocket] Eventbus listener registered for: ${prefixedEventName}`,
        )

        // Register corresponding socket listener
        fastify.wss.on("connection", (socket) => {
          socket.on(prefixedEventName, (payload: any) => {
            fastify.log.info(
              `[Socket] Event '${prefixedEventName}' received from socket.`,
            )
            handler(payload) // Execute the event handler
          })
        })
      })
    },

    emit: (eventName: string, data: unknown) => {
      // Whitelisted events will go to the main channel and need no prefix
      if (EVENT_WHITELIST.includes(eventName)) {
        fastify.log.info("[Eventbus] Emit Event " + eventName)
        fastify.eventbus.emit(eventName, data)
      } else {
        const prefixedEventName = `${pluginOptions.key}--${eventName}`

        fastify.log.info("[Eventbus] Emit Event " + prefixedEventName)
        fastify.eventbus.emit(prefixedEventName, data)
      }
    },

    registerSchema: (schemaDefinition: PluginSchemaDefinition) => {
      registerSchemaDefinition({
        ...schemaDefinition,
        pluginKey: pluginOptions.key,
        isCore: pluginOptions.type === "core",
      })
    },
    http: {
      get: fastify.http.get,
      post: fastify.http.post,
      put: fastify.http.put,
      delete: fastify.http.delete,
    } satisfies HttpService,

    createIntervalTask: (
      name: string,
      handler: () => Promise<void>,
      options: IntervalTaskOptions,
    ) => {
      const prefixedName = `${pluginOptions.key}--${name}`
      const pluginLogger = fastify.log.child({ plugin: pluginOptions.key })

      fastify.taskManager.createIntervalTask(prefixedName, handler, {
        ...options,
        errorHandler: (error) => {
          if (options.errorHandler) {
            options.errorHandler(error)
          }
          pluginLogger.error(`Task ${prefixedName} failed:`, error)
        },
      })
    },

    cancelTask: (name: string) => {
      const prefixedName = `${pluginOptions.key}--${name}`

      fastify.taskManager.cancelTask(prefixedName)
    },

    registerCrudHandlers: <T>(name: string, handlers: CrudHandlers<T>) => {
      const prefixedName = `${pluginOptions.key}--${name}`
      registerCrudHandlers(prefixedName, handlers, "plugin")
    },

    registerPayloadHandler: <T extends object>(
      name: string,
      registerData: any,
    ) => {
      const prefixedName = `${pluginOptions.key}--${name}`
      registerPayloadHandler<T>(prefixedName, registerData, pluginOptions.key)
    },

    createPayload: <T extends object>(
      name: string,
      item: T,
      requestData: FrontendPayloadRequestData,
      scope?: string,
    ): Promise<FrontendPayload<T[]>> => {
      const prefixedName = `${pluginOptions.key}--${name}`

      return createPayload<T>(
        prefixedName,
        item,
        requestData,
        scope || pluginOptions.key,
      )
    },

    eventbus: {
      emit: (eventName: string, data?: any) => {
        // TODO: add whitelist check where no prefix is applied
        const prefixedName = `${pluginOptions.key}--${eventName}`

        return fastify.eventbus.emit(prefixedName, data)
      },
      // TODO: fiy types
      on: (eventName: string, callback: Listener): EventEmitter => {
        const prefixedName = `${pluginOptions.key}--${eventName}`

        return fastify.eventbus.on(prefixedName, callback)
      },
    },

    registerPluginContract: (contractData: PluginContract) => {
      registerPluginContract(pluginOptions.key, contractData)
    },

    registerForFrontendGeneration: (
      options?: RegisterForFrontendGenerationInput,
    ) => {
      const fg = pluginOptions._loaderInternal?.frontendGeneration
      if (!fg) {
        if (options === false) {
          return
        }
        registerPluginForFrontendGeneration(
          pluginOptions,
          options === undefined ? {} : options,
        )
        return
      }
      if (options === false) {
        fg.optedOut = true
        return
      }
      fg.explicitCall = true
      registerPluginForFrontendGeneration(
        pluginOptions,
        options === undefined ? {} : options,
      )
    },

    cache: pluginCacheService,
  }

  const register: PluginFastifyInstance["register"] = (routes, options) => {
    const opts = {
      ...options,
      prefix: pluginOptions.routePrefix,
    }

    return fastify.register((encapsulatedInstance) => {
      const combined = new Proxy(instance, {
        // Use instance directly instead of this
        get: (target, prop) => {
          if (
            ["get", "post", "put", "patch", "delete"].includes(prop as string)
          ) {
            // @ts-ignore
            return encapsulatedInstance[prop].bind(encapsulatedInstance)
          }

          // @ts-ignore
          return target[prop]
        },
      })

      // @ts-ignore
      return routes(combined)
    }, opts)
  }

  instance.register = register
  instance.fastify = fastify
  instance.custom = {}

  return instance
}
