import type {
  FrontendPayloadRequestData,
  FrontendPayload,
  FrontendPayloadBody,
} from "@c/payload/payloadTypes"

// Required fields that must exist on payload item
interface RequiredPayloadFields {
  _id: string
  owner: string
  project: string
  updatedAt: string | Date
  createdAt: string | Date
  tags: string[]
}

interface OptionalPayloadFields {
  deleted?: boolean
}

// Base interface for any document that can be converted to a plain object
interface DocumentLike {
  toObject(): any
}

type RequestDataFallbackFields = "project"

type DataItem<T extends object> = T | (T & DocumentLike)

type MissingFields<T extends object> = Exclude<
  keyof RequiredPayloadFields,
  keyof T
>

type RequiredMissingFields<T extends object> = Exclude<
  MissingFields<T>,
  RequestDataFallbackFields
>

type RequiredMappings<T extends object> = Partial<RequiredPayloadFields> & {
  [K in RequiredMissingFields<T>]: RequiredPayloadFields[K]
}

// Payload handler configuration
interface PayloadHandlerConfig<T extends object, TMeta = any> {
  type: string
  displayName: (item: T) => string
  completion: (item: T) => string
  sanitize?: (item: T) => Partial<T>
  fields?: (
    item: T,
    requestData: FrontendPayloadRequestData,
  ) => RequiredMappings<T> & OptionalPayloadFields
  createMeta?: (item: T) => Promise<TMeta> | TMeta
  beforePayload?: (item: T) => Promise<T> | T
}

// Store handlers by scope, then by type
const handlers: Record<string, Record<string, PayloadHandlerConfig<any>>> = {
  core: {},
}

const isDocument = <T extends object>(item: T): item is T & DocumentLike => {
  return typeof (item as any)?.toObject === "function"
}

const toPlainObject = <T extends object>(item: DataItem<T>): T => {
  if (isDocument(item)) {
    return item.toObject()
  }

  return item
}

export const registerPayloadHandler = <T extends object, TMeta = any>(
  type: string,
  config: PayloadHandlerConfig<T, TMeta>,
  scope = "core",
): void => {
  // Ensure the scope exists
  if (!handlers[scope]) {
    handlers[scope] = {}
  }

  if (handlers[scope][type]) {
    throw new Error(
      `Payload handler for type "${type}" in scope "${scope}" already registered`,
    )
  }

  handlers[scope][type] = config
}

export const createPayloadBody = async <T extends object>(
  type: string,
  item: DataItem<T>,
  requestData: FrontendPayloadRequestData,
  scope = "core",
): Promise<FrontendPayloadBody<T>> => {
  // Check if the scope exists
  if (!handlers[scope]) {
    throw new Error(`No scope registered with name "${scope}"`)
  }

  const handler = handlers[scope][type]

  if (!handler) {
    throw new Error(
      `No payload handler registered for type "${type}" in scope "${scope}"`,
    )
  }

  let plainItem = toPlainObject(item)

  const sanitizedItem = handler.sanitize
    ? handler.sanitize(plainItem)
    : plainItem
  const meta = handler.createMeta
    ? await handler.createMeta(plainItem)
    : undefined
  const fields = handler.fields ? handler.fields(plainItem, requestData) : {}

  if (handler.beforePayload) {
    plainItem = await handler.beforePayload(plainItem)
  }

  const basePayload: Partial<FrontendPayloadBody<T>> = {
    _id: fields._id ?? (plainItem as any)._id ?? "",
    _type: handler.type,
    _offline: 0,
    _owner: fields.owner ?? (plainItem as any).owner,
    _tags: fields.tags ?? (plainItem as any).tags,
    _displayName: handler.displayName(plainItem) ?? "",
    _completion: handler.completion(plainItem) ?? "",
    _project:
      fields.project ?? (plainItem as any).project ?? requestData.project,
    _updatedAt: fields.updatedAt ?? (plainItem as any).updatedAt,
    _createdAt: fields.createdAt ?? (plainItem as any).createdAt,
    _deleted: fields.deleted ?? (plainItem as any).deleted ?? false,
    _source: sanitizedItem,
    _meta: meta,
  }

  return basePayload as FrontendPayloadBody<T>
}

// New function to process multiple items of the same type
export const createPayloadBodies = async <T extends object>(
  type: string,
  items: DataItem<T>[],
  requestData: FrontendPayloadRequestData,
  scope = "core",
): Promise<FrontendPayloadBody<T>[]> => {
  return await Promise.all(
    items.map((item) => createPayloadBody(type, item, requestData, scope)),
  )
}

// Updated function to handle arrays of items
export const createPayload = async <T extends object>(
  type: string,
  items: DataItem<T> | DataItem<T>[],
  requestData: FrontendPayloadRequestData,
  scope = "core",
): Promise<FrontendPayload<T[]>> => {
  // Convert single item to array if needed
  const itemsArray = Array.isArray(items) ? items : [items]

  // Create payload bodies for all items
  const bodies = await createPayloadBodies(type, itemsArray, requestData, scope)

  return {
    appSessionId: requestData?.appSessionId || "",
    appRequestCode: requestData?.appRequestCode || "",
    requestId: requestData?.requestId || "",
    type: requestData?.type || type,
    ...(requestData?.project ? { project: requestData?.project } : {}),
    body: {
      items: bodies,
    },
  }
}

// New function to create payloads for multiple items of different types
export const createMultiTypePayload = async <T extends object>(
  itemsByType: {
    type: string
    items: DataItem<T> | DataItem<T>[]
    scope?: string
  }[],
  requestData: FrontendPayloadRequestData,
): Promise<FrontendPayload<T>> => {
  const allItems: FrontendPayloadBody<T>[] = []

  // Process each type group
  await Promise.all(
    itemsByType.map(async ({ type, items, scope = "core" }) => {
      const itemsArray = Array.isArray(items) ? items : [items]
      const bodies = await createPayloadBodies(
        type,
        itemsArray,
        requestData,
        scope,
      )

      allItems.push(...bodies)
    }),
  )

  return {
    appSessionId: requestData.appSessionId || "",
    appRequestCode: requestData.appRequestCode || "",
    requestId: requestData.requestId || "",
    type: requestData.type || "multi",
    body: {
      items: allItems,
    },
  }
}

export const getPayloadHandler = <T extends object>(
  type: string,
  scope = "core",
): PayloadHandlerConfig<T> | undefined => {
  if (!handlers[scope]) {
    return undefined
  }

  return handlers[scope][type]
}

export const getPayloadHandlersByScope = (
  scope = "core",
): PayloadHandlerConfig<any>[] => {
  if (!handlers[scope]) {
    return []
  }

  return Object.values(handlers[scope])
}

export const getAllPayloadTypes = (scope = "core"): string[] => {
  if (!handlers[scope]) {
    return []
  }

  return Object.keys(handlers[scope])
}

export const getAllScopes = (): string[] => {
  return Object.keys(handlers)
}
