import {
  JSONValue,
  JSONObject,
  RecordData,
} from "@deepstream/client/dist/src/constants"

import { SubscriptionCallback as OriginalSubscriptionCallback } from "@deepstream/client/dist/src/record/record"

import * as log from "./log"

export { JSONValue, JSONObject, RecordData }
export type UserData = unknown

export type SubscriptionCallback =
  | OriginalSubscriptionCallback
  | ((...args: Parameters<OriginalSubscriptionCallback>) => Promise<void>)

/**
 * checks if `value` is JSONValue (JSON serializable)
 * logs error message if not
 *
 * @param value value to check
 * @param name the name of the value to use in debugging messages
 * @returns if the value is JSONValue
 */

export function isJSONValue(
  value: UserData,
  name = "unknown"
): value is JSONValue {
  const isJSON = validateJSONValue(value, "")
  if (!isJSON) {
    let details = ""
    if (validationError) {
      details = `\n"${name}${validationError.path}" ${validationError.message}`
    }
    log.warn(`User provided data is not JSON serializable.${details}`)
  }
  return isJSON
}

/**
 * checks if `value` is JSONObject (JSON serializable + object)
 * logs error message if not
 *
 * @param value value to check
 * @returns if the value is JSONObject
 */

export function isJSONObject(
  value: UserData,
  name?: string
): value is JSONObject {
  if (typeof value !== "object") {
    log.warn(`User provided data is not an object.`)
    return false
  }
  return isJSONValue(value, name)
}

export function isEmpty(o: any): boolean {
  if (typeof o !== "object") return false
  return Object.keys(o as object).length === 0
}

function defined(value: unknown): boolean {
  return typeof value !== "undefined"
}

/**
 * checks the given value to see if it is JSON serializable
 * recursively checks all objects and arrays
 *
 * @param value the value to validate
 * @param path the starting path, this is used for error messages only
 * (and is currently useless)
 * @returns if the value is JSON serializable
 */

interface ValidationError {
  path: string
  message: string
}

// validationError is a module global and can be checked if/after validateJSONValue returns false for info on the error.
// note: is there a better pattern for providing error details here?

let validationError: ValidationError | null

function validateJSONValue(value: UserData, path = ""): boolean {
  validationError = null

  // JSONPrimitives are okay
  if (["string", "number", "boolean"].includes(typeof value)) return true

  // nulls are okay
  if (value === null) return true

  // arrays okay, check children
  if (Array.isArray(value)) {
    for (const [i, v] of Object.entries(value)) {
      if (!validateJSONValue(v, `${path}[${i}]`)) return false
    }
    return true
  }

  // plain objects okay, check children
  if (typeof value === "object" && value.constructor === Object) {
    for (const [k, v] of Object.entries(value)) {
      if (!validateJSONValue(v, `${path}.${k}`)) return false
    }
    return true
  }

  if (typeof value === "function") {
    validationError = {
      path,
      message: `is a function. Functions are not allowed.`,
    }
    return false
  }
  if (typeof value === "symbol") {
    validationError = {
      path,
      message: `is a symbol. Symbols are not allowed.`,
    }
    return false
  }

  if (typeof value === "object") {
    // no warning for any objects not yet classified because its sometimes okay
    // for example the "stickies" example uses a Rect class with no methods
    // could possibly walk the prototype chain to see if there are any
    // properties that won't be shared and warn

    return true
  }

  if (typeof value === "undefined") {
    // allow it, key should be removed
    return true
  }

  validationError = {
    path,
    message: `is an unknown type. vanilla-party doesn't know what to do with it.`,
  }

  // if we got this far, we don't know what it is
  return false
}

/**
 *  JSONValue is a Deepstream type that reflects what is JSON serializable
 *
 *  checking a key on a JSONValue will warn because a JSONValue might
 *  not be an object, it could be a primitive.
 *
 *  .log((value as JSONObject).test); // okay
 *  .log(value.test);                 // not okay
 */
