import { CID } from 'multiformats/cid'
import { graphemeLen, utf8Len } from '@atproto/common-web'
import { Lexicons } from '../lexicons'
import {
  LexBoolean,
  LexBytes,
  LexInteger,
  LexString,
  LexUserType,
  ValidationError,
  ValidationResult,
} from '../types'
import * as formats from './formats'

export function validate(
  lexicons: Lexicons,
  path: string,
  def: LexUserType,
  value: unknown,
): ValidationResult {
  switch (def.type) {
    case 'boolean':
      return boolean(lexicons, path, def, value)
    case 'integer':
      return integer(lexicons, path, def, value)
    case 'string':
      return string(lexicons, path, def, value)
    case 'bytes':
      return bytes(lexicons, path, def, value)
    case 'cid-link':
      return cidLink(lexicons, path, def, value)
    case 'unknown':
      return unknown(lexicons, path, def, value)
    default:
      return {
        success: false,
        error: new ValidationError(`Unexpected lexicon type: ${def.type}`),
      }
  }
}

function boolean(
  lexicons: Lexicons,
  path: string,
  def: LexUserType,
  value: unknown,
): ValidationResult {
  def = def as LexBoolean

  // type
  const type = typeof value
  if (type === 'undefined') {
    if (typeof def.default === 'boolean') {
      return { success: true, value: def.default }
    }
    return {
      success: false,
      error: new ValidationError(`${path} must be a boolean`),
    }
  } else if (type !== 'boolean') {
    return {
      success: false,
      error: new ValidationError(`${path} must be a boolean`),
    }
  }

  // const
  if (typeof def.const === 'boolean') {
    if (value !== def.const) {
      return {
        success: false,
        error: new ValidationError(`${path} must be ${def.const}`),
      }
    }
  }

  return { success: true, value }
}

function integer(
  lexicons: Lexicons,
  path: string,
  def: LexUserType,
  value: unknown,
): ValidationResult {
  def = def as LexInteger

  // type
  const type = typeof value
  if (type === 'undefined') {
    if (typeof def.default === 'number') {
      return { success: true, value: def.default }
    }
    return {
      success: false,
      error: new ValidationError(`${path} must be an integer`),
    }
  } else if (!Number.isInteger(value)) {
    return {
      success: false,
      error: new ValidationError(`${path} must be an integer`),
    }
  }

  // const
  if (typeof def.const === 'number') {
    if (value !== def.const) {
      return {
        success: false,
        error: new ValidationError(`${path} must be ${def.const}`),
      }
    }
  }

  // enum
  if (Array.isArray(def.enum)) {
    if (!def.enum.includes(value as number)) {
      return {
        success: false,
        error: new ValidationError(
          `${path} must be one of (${def.enum.join('|')})`,
        ),
      }
    }
  }

  // maximum
  if (typeof def.maximum === 'number') {
    if ((value as number) > def.maximum) {
      return {
        success: false,
        error: new ValidationError(
          `${path} can not be greater than ${def.maximum}`,
        ),
      }
    }
  }

  // minimum
  if (typeof def.minimum === 'number') {
    if ((value as number) < def.minimum) {
      return {
        success: false,
        error: new ValidationError(
          `${path} can not be less than ${def.minimum}`,
        ),
      }
    }
  }

  return { success: true, value }
}

function string(
  lexicons: Lexicons,
  path: string,
  def: LexUserType,
  value: unknown,
): ValidationResult {
  def = def as LexString

  // type
  if (typeof value === 'undefined') {
    if (typeof def.default === 'string') {
      return { success: true, value: def.default }
    }
    return {
      success: false,
      error: new ValidationError(`${path} must be a string`),
    }
  } else if (typeof value !== 'string') {
    return {
      success: false,
      error: new ValidationError(`${path} must be a string`),
    }
  }

  // const
  if (typeof def.const === 'string') {
    if (value !== def.const) {
      return {
        success: false,
        error: new ValidationError(`${path} must be ${def.const}`),
      }
    }
  }

  // enum
  if (Array.isArray(def.enum)) {
    if (!def.enum.includes(value as string)) {
      return {
        success: false,
        error: new ValidationError(
          `${path} must be one of (${def.enum.join('|')})`,
        ),
      }
    }
  }

  // maxLength and minLength
  if (typeof def.minLength === 'number' || typeof def.maxLength === 'number') {
    // If the JavaScript string length * 3 is below the maximum limit,
    // its UTF8 length (which <= .length * 3) will also be below.
    if (typeof def.minLength === 'number' && value.length * 3 < def.minLength) {
      return {
        success: false,
        error: new ValidationError(
          `${path} must not be shorter than ${def.minLength} characters`,
        ),
      }
    }

    // If the JavaScript string length * 3 is within the maximum limit,
    // its UTF8 length (which <= .length * 3) will also be within.
    // When there's no minimal length, this lets us skip the UTF8 length check.
    let canSkipUtf8LenChecks = false
    if (
      typeof def.minLength === 'undefined' &&
      typeof def.maxLength === 'number' &&
      value.length * 3 <= def.maxLength
    ) {
      canSkipUtf8LenChecks = true
    }

    if (!canSkipUtf8LenChecks) {
      const len = utf8Len(value)

      if (typeof def.maxLength === 'number') {
        if (len > def.maxLength) {
          return {
            success: false,
            error: new ValidationError(
              `${path} must not be longer than ${def.maxLength} characters`,
            ),
          }
        }
      }

      if (typeof def.minLength === 'number') {
        if (len < def.minLength) {
          return {
            success: false,
            error: new ValidationError(
              `${path} must not be shorter than ${def.minLength} characters`,
            ),
          }
        }
      }
    }
  }

  // maxGraphemes and minGraphemes
  if (
    typeof def.maxGraphemes === 'number' ||
    typeof def.minGraphemes === 'number'
  ) {
    let needsMaxGraphemesCheck = false
    let needsMinGraphemesCheck = false

    if (typeof def.maxGraphemes === 'number') {
      if (value.length <= def.maxGraphemes) {
        // If the JavaScript string length (UTF-16) is within the maximum limit,
        // its grapheme length (which <= .length) will also be within.
        needsMaxGraphemesCheck = false
      } else {
        needsMaxGraphemesCheck = true
      }
    }

    if (typeof def.minGraphemes === 'number') {
      if (value.length < def.minGraphemes) {
        // If the JavaScript string length (UTF-16) is below the minimal limit,
        // its grapheme length (which <= .length) will also be below.
        // Fail early.
        return {
          success: false,
          error: new ValidationError(
            `${path} must not be shorter than ${def.minGraphemes} graphemes`,
          ),
        }
      } else {
        needsMinGraphemesCheck = true
      }
    }

    if (needsMaxGraphemesCheck || needsMinGraphemesCheck) {
      const len = graphemeLen(value)

      if (typeof def.maxGraphemes === 'number') {
        if (len > def.maxGraphemes) {
          return {
            success: false,
            error: new ValidationError(
              `${path} must not be longer than ${def.maxGraphemes} graphemes`,
            ),
          }
        }
      }

      if (typeof def.minGraphemes === 'number') {
        if (len < def.minGraphemes) {
          return {
            success: false,
            error: new ValidationError(
              `${path} must not be shorter than ${def.minGraphemes} graphemes`,
            ),
          }
        }
      }
    }
  }

  if (typeof def.format === 'string') {
    switch (def.format) {
      case 'datetime':
        return formats.datetime(path, value)
      case 'uri':
        return formats.uri(path, value)
      case 'at-uri':
        return formats.atUri(path, value)
      case 'did':
        return formats.did(path, value)
      case 'handle':
        return formats.handle(path, value)
      case 'at-identifier':
        return formats.atIdentifier(path, value)
      case 'nsid':
        return formats.nsid(path, value)
      case 'cid':
        return formats.cid(path, value)
      case 'language':
        return formats.language(path, value)
      case 'tid':
        return formats.tid(path, value)
      case 'record-key':
        return formats.recordKey(path, value)
    }
  }

  return { success: true, value }
}

function bytes(
  lexicons: Lexicons,
  path: string,
  def: LexUserType,
  value: unknown,
): ValidationResult {
  def = def as LexBytes

  if (!value || !(value instanceof Uint8Array)) {
    return {
      success: false,
      error: new ValidationError(`${path} must be a byte array`),
    }
  }

  // maxLength
  if (typeof def.maxLength === 'number') {
    if (value.byteLength > def.maxLength) {
      return {
        success: false,
        error: new ValidationError(
          `${path} must not be larger than ${def.maxLength} bytes`,
        ),
      }
    }
  }

  // minLength
  if (typeof def.minLength === 'number') {
    if (value.byteLength < def.minLength) {
      return {
        success: false,
        error: new ValidationError(
          `${path} must not be smaller than ${def.minLength} bytes`,
        ),
      }
    }
  }

  return { success: true, value }
}

function cidLink(
  lexicons: Lexicons,
  path: string,
  def: LexUserType,
  value: unknown,
): ValidationResult {
  if (CID.asCID(value) === null) {
    return {
      success: false,
      error: new ValidationError(`${path} must be a CID`),
    }
  }

  return { success: true, value }
}

function unknown(
  lexicons: Lexicons,
  path: string,
  def: LexUserType,
  value: unknown,
): ValidationResult {
  // type
  if (!value || typeof value !== 'object') {
    return {
      success: false,
      error: new ValidationError(`${path} must be an object`),
    }
  }

  return { success: true, value }
}
