import type { UiSchema } from '@rjsf/utils'

/** Simplified JSON schema for improved suggestions */
import { JSONSchema7, JSONSchema7Type, JSONSchema7TypeName } from 'json-schema'
import { toCamelCase, toKebabCase } from './utils.js'
import { BYOCDataTypes } from './datatypes.js'
export type JSONSchema7GenericKeys = Exclude<
  keyof JSONSchema7,
  // Type-specific properties
  | 'items'
  | 'additionalItems'
  | 'properties'
  | 'additionalProperties'
  | 'patternProperties'
  | 'dependencies'
  | 'required'
  | 'minProperties'
  | 'maxProperties'
  | 'minItems'
  | 'maxItems'
  | 'uniqueItems'
  | 'minLength'
  | 'maxLength'
  | 'pattern'
  | 'format'
  | 'minimum'
  | 'maximum'
  | 'multipleOf'
  | 'exclusiveMinimum'
  | 'exclusiveMaximum'
  | 'propertyNames'
  | 'contains'
  // Rarely used or more obscure properties
  | '$schema'
  | 'definitions'
  | '$defs'
  | '$comment'
  | 'const'
  | 'examples'
  | 'readOnly'
  | 'writeOnly'
  | 'contentMediaType'
  | 'contentEncoding'
  | 'if'
  | 'then'
  | 'else'
  | 'allOf'
  | 'anyOf'
  | 'oneOf'
  | 'not'
  // Advanced features
  | '$ref'
  | '$id'
>

/** Schema for common properties. */
export interface BaseSchema<T extends JSONSchema7TypeName> extends Pick<JSONSchema7, JSONSchema7GenericKeys> {
  /** The type of data. */
  type: T
  [key: string]: any
}

/** Schema for string type properties. */
export interface StringSchema extends BaseSchema<'string'> {
  /** Minimum length of the string. */
  minLength?: number
  /** Maximum length of the string. */
  maxLength?: number
  /** A regex pattern the string should match. */
  pattern?: string
  /** Named format the string should adhere to. */
  format?: string
}

/** Schema for number type properties. */
export interface NumberSchema extends BaseSchema<'number' | 'integer'> {
  /** Minimum value of the number. */
  minimum?: number
  /** Maximum value of the number. */
  maximum?: number
  /** The number should be a multiple of this value. */
  multipleOf?: number
}

/** Schema for object type properties. */
export interface ObjectSchema extends BaseSchema<'object'> {
  /** Properties of the object. */
  properties?: Record<string, JSONSchema>
  /** Additional properties not covered by 'properties'. */
  additionalProperties?: JSONSchema | boolean
  /** Array of required properties. */
  required?: string[]
  /** Minimum number of properties. */
  minProperties?: number
  /** Maximum number of properties. */
  maxProperties?: number
  /** Properties matching the patterns. */
  patternProperties?: Record<string, JSONSchema>
  /** Property dependencies. */
  dependencies?: Record<string, JSONSchema | string[]>
}

/** Schema for array type properties. */
export interface ArraySchema extends BaseSchema<'array'> {
  /** Items in the array. */
  items?: JSONSchema | JSONSchema[]
  /** Additional items not covered by 'items'. */
  additionalItems?: JSONSchema | boolean
  /** Minimum number of items. */
  minItems?: number
  /** Maximum number of items. */
  maxItems?: number
  /** All items should be unique. */
  uniqueItems?: boolean
}

/** Schema for boolean type properties. */
export interface BooleanSchema extends BaseSchema<'boolean'> {}

/** Schema for null type properties. */
export interface NullSchema extends BaseSchema<'null'> {}

/** Definition of JSON Schema type. */
export type JSONSchema = StringSchema | NumberSchema | ObjectSchema | ArraySchema | BooleanSchema | NullSchema

/**
 * JSON Schema is limited in describing how a given data type should be rendered as a form input component. That's why
 * this library introduces the concept of uiSchema.
 *
 * A UI schema is basically an object literal providing information on how the form should be rendered, while the JSON
 * schema tells what.
 *
 * @see https://rjsf-team.github.io/react-jsonschema-form/docs/api-reference/uiSchema
 * @example
 *
 * {
 *       // Present color choices as radio buttons
 *       color: { 'ui:widget': 'radio' },
 *       // Text is a textarea with 5 rows
 *       text: { 'ui:widget': 'textarea', 'ui:options': { rows: 5 } },
 *       // Count is a number field with spinner button
 *       count: { 'ui:widget': 'updown' }
 *     },
 *
 */

export type UISchema = UiSchema
export type JSONSchemaConstruct = Partial<ArraySchema | ObjectSchema>

/**
 * Iterate JSON schema including nested constructs like oneOf, allOf, if, then, etc. creating a copy in process.
 *  */
export function traverseSchema(
  schema: JSONSchemaConstruct,
  callback: (value: JSONSchemaConstruct) => JSONSchemaConstruct
): JSONSchemaConstruct {
  var traversed = {} as Partial<ArraySchema | ObjectSchema>
  for (var property in schema) {
    const value = schema[property]
    if (value && typeof value === 'object') {
      if (Array.isArray(value)) {
        traversed[property] = value.map((item) =>
          typeof item === 'object' && item ? traverseSchema(item, callback) : item
        )
      } else {
        traversed[property] = traverseSchema(callback(value), callback)
      }
    } else {
      traversed[property] = schema[property]
    }
  }
  return traversed
}

/**
 * Normalizes schema and assigns default values. It will traverse schema to normalize nested constructs.
 */
export function transformSchema(schema: Partial<ObjectSchema | ArraySchema>, defaults: Record<string, any> = {}) {
  schema = transformSchemaObject({
    /** 1. Assign schema type unless given */
    type: 'object',
    /** 2. Ensure that properties is not null */
    properties: {},
    required: [],
    ...schema
  })
  // Iterate nested constructs, without passing the defaults object
  return traverseSchema(schema, (construct) =>
    construct.properties ? transformSchemaObject(construct, defaults) : construct
  )
}
/**
 * Normalizes schema and assigns default values
 */
export function transformSchemaObject(
  schema: Partial<ObjectSchema | ArraySchema>,
  defaults: Record<string, any> = {}
): ObjectSchema | ArraySchema {
  const transformed = { ...schema }
  if (transformed.properties) {
    transformed.required = schema.required?.slice() || []
    transformed.properties = Object.keys(transformed.properties).reduce((acc: ObjectSchema['properties'], key) => {
      const given = transformed.properties[key]
      const custom = BYOCDataTypes[given?.type]?.(given)
      const property = {
        ...custom,
        ...given,
        type: custom?.type || given.type,
        /** 3. Assign default value from the explicit defaults object*/
        default: defaults.hasOwnProperty(key) ? defaults[key] : given.default,
        /** 4. Generate fallback title */
        title:
          given.title ||
          toKebabCase(key)
            .split('-')
            .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
            .join(' ')
      }
      if (given?.properties || custom?.properties) {
        property.properties = {
          ...(custom?.properties || given?.properties)
        }
        // Auto-merge property definitions if their names match. Will bail out when expanding nested objects like Item and List
        Object.keys(custom?.properties || {}).forEach((subKey) => {
          property.properties[subKey] = { ...custom?.properties?.[subKey], ...given?.properties?.[subKey] }
        })
      }
      if (given?.items || custom?.items) {
        property.items = custom?.items || given?.items
      }
      if (property.default === undefined) {
        delete property.default
      }
      if ('required' in property && typeof property.required == 'boolean') {
        if (transformed.required.indexOf(key) == -1) {
          transformed.required.push(key)
        }
        delete property.required
      }
      return Object.assign(acc as any, {
        [key]: property
      })
    }, {}) as Partial<ObjectSchema>
  }
  return {
    type: 'object',
    ...transformed
  } as ObjectSchema
}
/** Make UI schema assumptions to improve the ui */
export const transformUiSchema = (uiSchema: UISchema, properties: Record<string, any>) => {
  let transformed: UISchema = {
    ...uiSchema
  }

  for (var property in properties) {
    if (typeof properties[property] != 'object' || !properties[property]) {
      continue
    }
    if (properties[property]?.type == 'integer' || properties[property]?.type == 'number') {
      transformed[property] = {
        ...transformed[property],
        'ui:options': {
          widget: 'updown'
        }
      }
    }
    if (properties[property]?.ui) {
      transformed[property] = {
        ...transformed[property],
        'ui:options': {
          ...transformed[property]?.['ui:options'],
          ...properties[property].ui
        }
      }
    }
    transformed = {
      ...transformed,
      [property]: properties[property].items
        ? { items: transformUiSchema(transformed[property], properties[property].items.properties) }
        : transformUiSchema(transformed[property], properties[property].properties || {})
    }
  }

  return transformed
}

/** Parses a property value based on its property name and type defined in the JSON properties object. */
export function parseValue(value: any, type: JSONSchema['type']) {
  switch (type) {
    case 'string':
      return value
    case 'object':
      try {
        return typeof value == 'object' && value != null ? value : JSON.parse(value)
      } catch (e) {
        return null
      }
    case 'array':
      try {
        return Array.isArray(value) ? value : JSON.parse(value)
      } catch (e) {
        return null
      }
    case 'number':
      return parseFloat(value)
    case 'integer':
      return parseInt(value)
    case 'boolean':
      return value == 'true' || value == '1'
    default:
      return value
  }
}

/** Transform properties to match the schema types of a specified component. It will parse json for objects and arrays. */
export function parseSchemaProperties(schema: JSONSchema, props: Record<string, string>): any {
  return Object.keys(props).reduce((prev, name) => {
    const value = props[name]
    const prop = toCamelCase(name)
    const definition = schema?.properties[prop]
    const type = definition?.type
    const parsed = parseValue(value, type)
    if (parsed != null && !name.startsWith('data-attribute') && !['class', 'id', 'contenteditable'].includes(name)) {
      return { ...prev, [prop]: parsed }
    } else {
      return prev
    }
  }, {})
}
/**
 * 1. Transform properties to match the schema types of a specified component. It will parse json for objects and arrays.
 * 2. Combine it with default values as defined by the schema
 */
export function getSchemaProperties(schema: JSONSchema, props: Record<string, string>): any {
  return {
    ...getSchemaDefaults(schema),
    ...parseSchemaProperties(schema, props)
  }
}
/** Get properties with their default non-null values*/
export function getSchemaDefaults(schema: JSONSchema): any {
  return Object.keys(schema.properties).reduce((prev, prop) => {
    if (schema.properties[prop]?.default != null) {
      return { ...prev, [prop]: schema.properties[prop]?.default }
    } else {
      return prev
    }
  }, {})
}
