import {
  type CustomValidator,
  isKeyedObject,
  isSlug,
  type Path,
  type SlugIsUniqueValidator,
  type SlugParent,
  type SlugSchemaType,
  type SlugValidationContext,
} from '@sanity/types'
import {memoize} from 'lodash'

const memoizedWarnOnArraySlug = memoize(warnOnArraySlug)

function getDocumentIds(id: string) {
  const isDraft = id.indexOf('drafts.') === 0
  return {
    published: isDraft ? id.slice('drafts.'.length) : id,
    draft: isDraft ? id : `drafts.${id}`,
  }
}

function serializePath(path: Path): string {
  return path.reduce<string>((target, part, i) => {
    const isIndex = typeof part === 'number'
    const isKey = isKeyedObject(part)
    const separator = i === 0 ? '' : '.'
    const add = isIndex || isKey ? '[]' : `${separator}${part}`
    return `${target}${add}`
  }, '')
}

const defaultIsUnique: SlugIsUniqueValidator = (slug, context) => {
  const {getClient, document, path, type} = context
  const schemaOptions = type?.options as {disableArrayWarning?: boolean} | undefined

  if (!document) {
    throw new Error(`\`document\` was not provided in validation context.`)
  }
  if (!path) {
    throw new Error(`\`path\` was not provided in validation context.`)
  }

  const disableArrayWarning = schemaOptions?.disableArrayWarning || false
  const {published, draft} = getDocumentIds(document._id)
  const docType = document._type
  const atPath = serializePath(path.concat('current'))

  if (!disableArrayWarning && atPath.includes('[]') && context.environment === 'studio') {
    memoizedWarnOnArraySlug(serializePath(path))
  }

  const constraints = [
    '_type == $docType',
    `!(_id in [$draft, $published])`,
    `${atPath} == $slug`,
  ].join(' && ')

  return getClient({apiVersion: '2023-11-13'}).fetch<boolean>(
    `!defined(*[${constraints}][0]._id)`,
    {
      docType,
      draft,
      published,
      slug,
    },
    {tag: 'validation.slug-is-unique'},
  )
}

function warnOnArraySlug(serializedPath: string) {
  /* eslint-disable no-console */
  console.warn(
    [
      `Slug field at path ${serializedPath} is within an array and cannot be automatically checked for uniqueness`,
      `If you need to check for uniqueness, provide your own "isUnique" method`,
      `To disable this message, set \`disableArrayWarning: true\` on the slug \`options\` field`,
    ].join('\n'),
  )
  /* eslint-enable no-console */
}

/**
 * Validates slugs values by querying for uniqueness from the client.
 *
 * This is a custom rule implementation (e.g. `Rule.custom(slugValidator)`)
 * that's populated in `inferFromSchemaType` when the type name is `slug`
 */
export const slugValidator: CustomValidator = async (value, context) => {
  if (!value) {
    return true
  }

  const {i18n} = context

  if (typeof value !== 'object' || Array.isArray(value)) {
    return i18n.t('validation:slug.not-object')
  }

  if (!isSlug(value) || value.current.trim().length === 0) {
    return i18n.t('validation:slug.missing-current')
  }

  const options = context?.type?.options as {isUnique?: SlugIsUniqueValidator} | undefined
  const isUnique = options?.isUnique || defaultIsUnique

  const slugContext: SlugValidationContext = {
    ...context,
    parent: context.parent as SlugParent,
    type: context.type as SlugSchemaType,
    defaultIsUnique,
  }

  const wasUnique = await isUnique(value.current, slugContext)
  if (wasUnique) {
    return true
  }

  return i18n.t('validation:slug.not-unique', {slug: value.current})
}
