import * as utils from '../../../utils'
import { zuiKey } from '../../consts'
import {
  type IZodString,
  type IZodType,
  ZodStringCheck,
  ZodStringDef,
  ParseContext,
  ParseInput,
  ParseReturnType,
} from '../../typings'
import { ZodBaseTypeImpl, addIssueToContext, ParseStatus } from '../basetype'

export type { ZodStringCheck, ZodStringDef }
export type IpVersion = NonNullable<Extract<ZodStringCheck, { kind: 'ip' }>['version']>

export const cuidRegex = /^c[^\s-]{8,}$/i
export const cuid2Regex = /^[a-z][a-z0-9]*$/
export const ulidRegex = /^[0-9A-HJKMNP-TV-Z]{26}$/
export const uuidRegex = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/i
// from https://stackoverflow.com/a/46181/1550155
// old version: too slow, didn't support unicode

export const emailRegex = /^(?!\.)(?!.*\.\.)([A-Z0-9_+-\.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9\-]*\.)+[A-Z]{2,}$/i

// from https://thekevinscott.com/emojis-in-javascript/#writing-a-regular-expression
const _emojiRegex = '^(\\p{Extended_Pictographic}|\\p{Emoji_Component})+$'

let emojiRegex: RegExp
export const ipv4Regex =
  /^(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))$/
export const ipv6Regex =
  /^(([a-f0-9]{1,4}:){7}|::([a-f0-9]{1,4}:){0,6}|([a-f0-9]{1,4}:){1}:([a-f0-9]{1,4}:){0,5}|([a-f0-9]{1,4}:){2}:([a-f0-9]{1,4}:){0,4}|([a-f0-9]{1,4}:){3}:([a-f0-9]{1,4}:){0,3}|([a-f0-9]{1,4}:){4}:([a-f0-9]{1,4}:){0,2}|([a-f0-9]{1,4}:){5}:([a-f0-9]{1,4}:){0,1})([a-f0-9]{1,4}|(((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2}))\.){3}((25[0-5])|(2[0-4][0-9])|(1[0-9]{2})|([0-9]{1,2})))$/

function isValidIP(ip: string, version?: IpVersion) {
  if ((version === 'v4' || !version) && ipv4Regex.test(ip)) {
    return true
  }
  if ((version === 'v6' || !version) && ipv6Regex.test(ip)) {
    return true
  }

  return false
}

export class ZodStringImpl extends ZodBaseTypeImpl<string, ZodStringDef> implements IZodString {
  _parse(input: ParseInput): ParseReturnType<string> {
    if (this._def.coerce) {
      input.data = String(input.data)
    }
    const parsedType = this._getType(input)

    if (parsedType !== 'string') {
      const ctx = this._getOrReturnCtx(input)
      addIssueToContext(
        ctx,
        {
          code: 'invalid_type',
          expected: 'string',
          received: ctx.parsedType,
        }
        //
      )
      return { status: 'aborted' }
    }

    const status = new ParseStatus()
    let ctx: undefined | ParseContext = undefined

    for (const check of this._def.checks) {
      if (check.kind === 'min') {
        if (input.data.length < check.value) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            code: 'too_small',
            minimum: check.value,
            type: 'string',
            inclusive: true,
            exact: false,
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'max') {
        if (input.data.length > check.value) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            code: 'too_big',
            maximum: check.value,
            type: 'string',
            inclusive: true,
            exact: false,
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'length') {
        const tooBig = input.data.length > check.value
        const tooSmall = input.data.length < check.value
        if (tooBig || tooSmall) {
          ctx = this._getOrReturnCtx(input, ctx)
          if (tooBig) {
            addIssueToContext(ctx, {
              code: 'too_big',
              maximum: check.value,
              type: 'string',
              inclusive: true,
              exact: true,
              message: check.message,
            })
          } else if (tooSmall) {
            addIssueToContext(ctx, {
              code: 'too_small',
              minimum: check.value,
              type: 'string',
              inclusive: true,
              exact: true,
              message: check.message,
            })
          }
          status.dirty()
        }
      } else if (check.kind === 'email') {
        if (!emailRegex.test(input.data)) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            validation: 'email',
            code: 'invalid_string',
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'emoji') {
        if (!emojiRegex) {
          emojiRegex = new RegExp(_emojiRegex, 'u')
        }
        if (!emojiRegex.test(input.data)) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            validation: 'emoji',
            code: 'invalid_string',
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'uuid') {
        if (!uuidRegex.test(input.data)) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            validation: 'uuid',
            code: 'invalid_string',
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'cuid') {
        if (!cuidRegex.test(input.data)) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            validation: 'cuid',
            code: 'invalid_string',
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'cuid2') {
        if (!cuid2Regex.test(input.data)) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            validation: 'cuid2',
            code: 'invalid_string',
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'ulid') {
        if (!ulidRegex.test(input.data)) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            validation: 'ulid',
            code: 'invalid_string',
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'url') {
        try {
          new URL(input.data)
        } catch {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            validation: 'url',
            code: 'invalid_string',
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'regex') {
        check.regex.lastIndex = 0
        const testResult = check.regex.test(input.data)
        if (!testResult) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            validation: 'regex',
            code: 'invalid_string',
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'trim') {
        input.data = input.data.trim()
      } else if (check.kind === 'includes') {
        if (!(input.data as string).includes(check.value, check.position)) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            code: 'invalid_string',
            validation: { includes: check.value, position: check.position },
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'toLowerCase') {
        input.data = input.data.toLowerCase()
      } else if (check.kind === 'toUpperCase') {
        input.data = input.data.toUpperCase()
      } else if (check.kind === 'startsWith') {
        if (!(input.data as string).startsWith(check.value)) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            code: 'invalid_string',
            validation: { startsWith: check.value },
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'endsWith') {
        if (!(input.data as string).endsWith(check.value)) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            code: 'invalid_string',
            validation: { endsWith: check.value },
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'datetime') {
        const regex = utils.datestring.generateDatetimeRegex(check)

        if (!regex.test(input.data)) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            code: 'invalid_string',
            validation: 'datetime',
            message: check.message,
          })
          status.dirty()
        }
      } else if (check.kind === 'ip') {
        if (!isValidIP(input.data, check.version)) {
          ctx = this._getOrReturnCtx(input, ctx)
          addIssueToContext(ctx, {
            validation: 'ip',
            code: 'invalid_string',
            message: check.message,
          })
          status.dirty()
        }
      } else {
        utils.assert.assertNever(check)
      }
    }

    return { status: status.value, value: input.data }
  }

  _addCheck(check: ZodStringCheck) {
    return new ZodStringImpl({
      ...this._def,
      checks: [...this._def.checks, check],
    })
  }

  email(message?: utils.errors.ErrMessage) {
    return this._addCheck({ kind: 'email', ...utils.errors.errToObj(message) })
  }
  url(message?: utils.errors.ErrMessage) {
    return this._addCheck({ kind: 'url', ...utils.errors.errToObj(message) })
  }
  emoji(message?: utils.errors.ErrMessage) {
    return this._addCheck({ kind: 'emoji', ...utils.errors.errToObj(message) })
  }
  uuid(message?: utils.errors.ErrMessage) {
    return this._addCheck({ kind: 'uuid', ...utils.errors.errToObj(message) })
  }
  cuid(message?: utils.errors.ErrMessage) {
    return this._addCheck({ kind: 'cuid', ...utils.errors.errToObj(message) })
  }
  cuid2(message?: utils.errors.ErrMessage) {
    return this._addCheck({ kind: 'cuid2', ...utils.errors.errToObj(message) })
  }
  ulid(message?: utils.errors.ErrMessage) {
    return this._addCheck({ kind: 'ulid', ...utils.errors.errToObj(message) })
  }

  ip(options?: string | { version?: 'v4' | 'v6'; message?: string }) {
    return this._addCheck({ kind: 'ip', ...utils.errors.errToObj(options) })
  }

  datetime(
    options?:
      | string
      | {
          message?: string | undefined
          precision?: number | null
          offset?: boolean
        }
  ) {
    if (typeof options === 'string') {
      return this._addCheck({
        kind: 'datetime',
        precision: null,
        offset: false,
        message: options,
      })
    }
    return this._addCheck({
      kind: 'datetime',
      precision: typeof options?.precision === 'undefined' ? null : options?.precision,
      offset: options?.offset ?? false,
      ...utils.errors.errToObj(options?.message),
    })
  }

  regex(regex: RegExp, message?: utils.errors.ErrMessage) {
    return this._addCheck({
      kind: 'regex',
      regex,
      ...utils.errors.errToObj(message),
    })
  }

  includes(value: string, options?: { message?: string; position?: number }) {
    return this._addCheck({
      kind: 'includes',
      value,
      position: options?.position,
      ...utils.errors.errToObj(options?.message),
    })
  }

  startsWith(value: string, message?: utils.errors.ErrMessage) {
    return this._addCheck({
      kind: 'startsWith',
      value,
      ...utils.errors.errToObj(message),
    })
  }

  endsWith(value: string, message?: utils.errors.ErrMessage) {
    return this._addCheck({
      kind: 'endsWith',
      value,
      ...utils.errors.errToObj(message),
    })
  }

  min(minLength: number, message?: utils.errors.ErrMessage) {
    return this._addCheck({
      kind: 'min',
      value: minLength,
      ...utils.errors.errToObj(message),
    })
  }

  max(maxLength: number, message?: utils.errors.ErrMessage) {
    return this._addCheck({
      kind: 'max',
      value: maxLength,
      ...utils.errors.errToObj(message),
    })
  }

  length(len: number, message?: utils.errors.ErrMessage) {
    return this._addCheck({
      kind: 'length',
      value: len,
      ...utils.errors.errToObj(message),
    })
  }

  /**
   * @deprecated Use z.string().min(1) instead.
   * @see {@link ZodString.min}
   */
  nonempty(message?: utils.errors.ErrMessage) {
    return this.min(1, utils.errors.errToObj(message))
  }

  trim() {
    return new ZodStringImpl({
      ...this._def,
      checks: [...this._def.checks, { kind: 'trim' }],
    })
  }

  secret() {
    this._def[zuiKey] = { ...this._def[zuiKey], secret: true }
    return this
  }

  toLowerCase() {
    return new ZodStringImpl({
      ...this._def,
      checks: [...this._def.checks, { kind: 'toLowerCase' }],
    })
  }

  toUpperCase() {
    return new ZodStringImpl({
      ...this._def,
      checks: [...this._def.checks, { kind: 'toUpperCase' }],
    })
  }

  get isDatetime() {
    return !!this._def.checks.find((ch) => ch.kind === 'datetime')
  }

  get isEmail() {
    return !!this._def.checks.find((ch) => ch.kind === 'email')
  }
  get isURL() {
    return !!this._def.checks.find((ch) => ch.kind === 'url')
  }
  get isEmoji() {
    return !!this._def.checks.find((ch) => ch.kind === 'emoji')
  }
  get isUUID() {
    return !!this._def.checks.find((ch) => ch.kind === 'uuid')
  }
  get isCUID() {
    return !!this._def.checks.find((ch) => ch.kind === 'cuid')
  }
  get isCUID2() {
    return !!this._def.checks.find((ch) => ch.kind === 'cuid2')
  }
  get isULID() {
    return !!this._def.checks.find((ch) => ch.kind === 'ulid')
  }
  get isIP() {
    return !!this._def.checks.find((ch) => ch.kind === 'ip')
  }

  get minLength() {
    let min: number | null = null
    for (const ch of this._def.checks) {
      if (ch.kind === 'min') {
        if (min === null || ch.value > min) min = ch.value
      }
    }
    return min
  }
  get maxLength() {
    let max: number | null = null
    for (const ch of this._def.checks) {
      if (ch.kind === 'max') {
        if (max === null || ch.value < max) max = ch.value
      }
    }
    return max
  }

  isEqual(schema: IZodType): boolean {
    if (!(schema instanceof ZodStringImpl)) return false
    const thisChecks = new utils.ds.CustomSet<ZodStringCheck>(this._def.checks)
    const thatChecks = new utils.ds.CustomSet<ZodStringCheck>(schema._def.checks)
    return thisChecks.isEqual(thatChecks)
  }
}
