import { ArrayType, Type } from './types'
import type { ArrayItems, Field, LLMNodeType } from './types'
import type { Schema, ValidationError } from 'jsonschema'
import { Validator } from 'jsonschema'
import produce from 'immer'
import { z } from 'zod'

export const checkNodeValid = (_payload: LLMNodeType) => {
  return true
}

export const getFieldType = (field: Field) => {
  const { type, items } = field
  if (type !== Type.array || !items)
    return type

  return ArrayType[items.type]
}

export const getHasChildren = (schema: Field) => {
  const complexTypes = [Type.object, Type.array]
  if (!complexTypes.includes(schema.type))
    return false
  if (schema.type === Type.object)
    return schema.properties && Object.keys(schema.properties).length > 0
  if (schema.type === Type.array)
    return schema.items && schema.items.type === Type.object && schema.items.properties && Object.keys(schema.items.properties).length > 0
}

export const getTypeOf = (target: any) => {
  if (target === null) return 'null'
  if (typeof target !== 'object') {
    return typeof target
  }
  else {
    return Object.prototype.toString
      .call(target)
      .slice(8, -1)
      .toLocaleLowerCase()
  }
}

export const inferType = (value: any): Type => {
  const type = getTypeOf(value)
  if (type === 'array') return Type.array
  // type boolean will be treated as string
  if (type === 'boolean') return Type.string
  if (type === 'number') return Type.number
  if (type === 'string') return Type.string
  if (type === 'object') return Type.object
  return Type.string
}

export const jsonToSchema = (json: any): Field => {
  const schema: Field = {
    type: inferType(json),
  }

  if (schema.type === Type.object) {
    schema.properties = {}
    schema.required = []
    schema.additionalProperties = false

    Object.entries(json).forEach(([key, value]) => {
      schema.properties![key] = jsonToSchema(value)
      schema.required!.push(key)
    })
  }
  else if (schema.type === Type.array) {
    schema.items = jsonToSchema(json[0]) as ArrayItems
  }

  return schema
}

export const checkJsonDepth = (json: any) => {
  if (!json || getTypeOf(json) !== 'object')
    return 0

  let maxDepth = 0

  if (getTypeOf(json) === 'array') {
    if (json[0] && getTypeOf(json[0]) === 'object')
      maxDepth = checkJsonDepth(json[0])
  }
  else if (getTypeOf(json) === 'object') {
    const propertyDepths = Object.values(json).map(value => checkJsonDepth(value))
    maxDepth = propertyDepths.length ? Math.max(...propertyDepths) + 1 : 1
  }

  return maxDepth
}

export const checkJsonSchemaDepth = (schema: Field) => {
  if (!schema || getTypeOf(schema) !== 'object')
    return 0

  let maxDepth = 0

  if (schema.type === Type.object && schema.properties) {
    const propertyDepths = Object.values(schema.properties).map(value => checkJsonSchemaDepth(value))
    maxDepth = propertyDepths.length ? Math.max(...propertyDepths) + 1 : 1
  }
  else if (schema.type === Type.array && schema.items && schema.items.type === Type.object) {
    maxDepth = checkJsonSchemaDepth(schema.items) + 1
  }

  return maxDepth
}

export const findPropertyWithPath = (target: any, path: string[]) => {
  let current = target
  for (const key of path)
    current = current[key]
  return current
}

const draft07MetaSchema = {
  $schema: 'http://json-schema.org/draft-07/schema#',
  $id: 'http://json-schema.org/draft-07/schema#',
  title: 'Core schema meta-schema',
  definitions: {
    schemaArray: {
      type: 'array',
      minItems: 1,
      items: { $ref: '#' },
    },
    nonNegativeInteger: {
      type: 'integer',
      minimum: 0,
    },
    nonNegativeIntegerDefault0: {
      allOf: [
        { $ref: '#/definitions/nonNegativeInteger' },
        { default: 0 },
      ],
    },
    simpleTypes: {
      enum: [
        'array',
        'boolean',
        'integer',
        'null',
        'number',
        'object',
        'string',
      ],
    },
    stringArray: {
      type: 'array',
      items: { type: 'string' },
      uniqueItems: true,
      default: [],
    },
  },
  type: ['object', 'boolean'],
  properties: {
    $id: {
      type: 'string',
      format: 'uri-reference',
    },
    $schema: {
      type: 'string',
      format: 'uri',
    },
    $ref: {
      type: 'string',
      format: 'uri-reference',
    },
    title: {
      type: 'string',
    },
    description: {
      type: 'string',
    },
    default: true,
    readOnly: {
      type: 'boolean',
      default: false,
    },
    examples: {
      type: 'array',
      items: true,
    },
    multipleOf: {
      type: 'number',
      exclusiveMinimum: 0,
    },
    maximum: {
      type: 'number',
    },
    exclusiveMaximum: {
      type: 'number',
    },
    minimum: {
      type: 'number',
    },
    exclusiveMinimum: {
      type: 'number',
    },
    maxLength: { $ref: '#/definitions/nonNegativeInteger' },
    minLength: { $ref: '#/definitions/nonNegativeIntegerDefault0' },
    pattern: {
      type: 'string',
      format: 'regex',
    },
    additionalItems: { $ref: '#' },
    items: {
      anyOf: [
        { $ref: '#' },
        { $ref: '#/definitions/schemaArray' },
      ],
      default: true,
    },
    maxItems: { $ref: '#/definitions/nonNegativeInteger' },
    minItems: { $ref: '#/definitions/nonNegativeIntegerDefault0' },
    uniqueItems: {
      type: 'boolean',
      default: false,
    },
    contains: { $ref: '#' },
    maxProperties: { $ref: '#/definitions/nonNegativeInteger' },
    minProperties: { $ref: '#/definitions/nonNegativeIntegerDefault0' },
    required: { $ref: '#/definitions/stringArray' },
    additionalProperties: { $ref: '#' },
    definitions: {
      type: 'object',
      additionalProperties: { $ref: '#' },
      default: {},
    },
    properties: {
      type: 'object',
      additionalProperties: { $ref: '#' },
      default: {},
    },
    patternProperties: {
      type: 'object',
      additionalProperties: { $ref: '#' },
      propertyNames: { format: 'regex' },
      default: {},
    },
    dependencies: {
      type: 'object',
      additionalProperties: {
        anyOf: [
          { $ref: '#' },
          { $ref: '#/definitions/stringArray' },
        ],
      },
    },
    propertyNames: { $ref: '#' },
    const: true,
    enum: {
      type: 'array',
      items: true,
      minItems: 1,
      uniqueItems: true,
    },
    type: {
      anyOf: [
        { $ref: '#/definitions/simpleTypes' },
        {
          type: 'array',
          items: { $ref: '#/definitions/simpleTypes' },
          minItems: 1,
          uniqueItems: true,
        },
      ],
    },
    format: { type: 'string' },
    allOf: { $ref: '#/definitions/schemaArray' },
    anyOf: { $ref: '#/definitions/schemaArray' },
    oneOf: { $ref: '#/definitions/schemaArray' },
    not: { $ref: '#' },
  },
  default: true,
} as unknown as Schema

const validator = new Validator()

export const validateSchemaAgainstDraft7 = (schemaToValidate: any) => {
  const schema = produce(schemaToValidate, (draft: any) => {
    // Make sure the schema has the $schema property for draft-07
    if (!draft.$schema)
      draft.$schema = 'http://json-schema.org/draft-07/schema#'
  })

  const result = validator.validate(schema, draft07MetaSchema, {
    nestedErrors: true,
    throwError: false,
  })

  // Access errors from the validation result
  const errors = result.valid ? [] : result.errors || []

  return errors
}

export const getValidationErrorMessage = (errors: ValidationError[]) => {
  const message = errors.map((error) => {
    return `Error: ${error.path.join('.')} ${error.message} Details: ${JSON.stringify(error.stack)}`
  }).join('; ')
  return message
}

// Previous Not support boolean type, so transform boolean to string when paste it into schema editor
export const convertBooleanToString = (schema: any) => {
  if (schema.type === Type.boolean)
    schema.type = Type.string
  if (schema.type === Type.array && schema.items && schema.items.type === Type.boolean)
    schema.items.type = Type.string
  if (schema.type === Type.object) {
    schema.properties = Object.entries(schema.properties).reduce((acc, [key, value]) => {
      acc[key] = convertBooleanToString(value)
      return acc
    }, {} as any)
  }
  if (schema.type === Type.array && schema.items && schema.items.type === Type.object) {
    schema.items.properties = Object.entries(schema.items.properties).reduce((acc, [key, value]) => {
      acc[key] = convertBooleanToString(value)
      return acc
    }, {} as any)
  }
  return schema
}

const schemaRootObject = z.object({
  type: z.literal('object'),
  properties: z.record(z.string(), z.any()),
  required: z.array(z.string()),
  additionalProperties: z.boolean().optional(),
})

export const preValidateSchema = (schema: any) => {
  const result = schemaRootObject.safeParse(schema)
  return result
}
