import crypto from 'node:crypto'
import { Config } from './config'
// import { INDEX_TYPE, TYPE_INDEX, TYPE, AcceptedValue } from './types'
import { TYPE, AcceptedValue } from './types'
import { UrlSafeBase64 } from '@/utils/base64'
import { sha256 } from '@/hash'

export class Factory {
  private separator: string = '\x1f'
  private encoding: BufferEncoding = 'utf8'
  public config: Config

  public lastConfig: Config | null = null

  constructor(config?: Record<string, any> | Config) {
    this.config = new Config()
    this.setConfig(config || new Config())
  }

  public same(): this {
    this.config.entropy = 0
    // $this->config->seed = hexdec(md5($this->config->key ?? ''))
    return this.setConfig(this.config)
  }

  public unique(): this {
    this.config.entropy = 4
    return this.setConfig(this.config)
  }

  // TODO: review it..
  public setConfig(config: Record<string, any> | Config): this {
    if (!(config instanceof Config))
      config = new Config(config)

    this.config = Object.assign(new Config(), config)
    return this
  }

  public onceConfig(config: Record<string, any> | Config): this {
    if (!(config instanceof Config))
      config = new Config(config)

    this.lastConfig = this.config
    this.config = Object.assign(new Config(), config)
    return this
  }

  public onceKey(secret: string): this {
    this.lastConfig = Object.assign(new Config(), this.config)
    this.config.key = secret
    return this
  }

  private maybeRestoreConfig(): this {
    if (this.lastConfig !== null) {
      this.config = Object.assign(new Config(), this.lastConfig)
      this.lastConfig = null
    }

    return this
  }

  private validateConfig(): void {
    const key = this.config.key
    if (typeof key !== 'string' || !key.trim())
      throw new Error('Secret key is required for use cripta.')
  }

  public encode(data: AcceptedValue): string {
    this.validateConfig()

    const entropy = this.config.getInt('entropy')
    const entropyBytes = entropy ? crypto.randomBytes(entropy) : Buffer.from('')

    const encoded = this.baseEncode(
      Buffer.concat([
        Buffer.from(this.cipher(
          this.toText(data),
          this.forgeKey(this.config.key as string, entropyBytes.toString('binary'))
        ), 'binary'),
        entropyBytes
      ])
    )

    this.maybeRestoreConfig()
    return encoded
  }

  public decode(data: string): AcceptedValue {
    this.validateConfig()

    const binary = this.baseDecode(data)
    const eLength = this.config.getInt('entropy')

    const entropyBytes = binary.slice(-eLength)
    const cipherBytes = binary.slice(0, -eLength || undefined)

    const decoded = this.convert(this.cipher(
      cipherBytes.toString(),
      this.forgeKey(this.config.key as string, entropyBytes.toString())
    ))

    this.maybeRestoreConfig()
    return decoded
  }

  private convert(value: string): AcceptedValue {
    const separatorIndex = value.indexOf(this.separator)

    if (separatorIndex !== -1) {
      const typeStr = value.substring(0, separatorIndex)
      const typeNum = parseInt(typeStr, 10)
      const data = value.substring(separatorIndex + 1)

      switch (typeNum) {
        case TYPE.null:
          return null
        case TYPE.boolean:
          return data === 'true' || data === '1'
        case TYPE.string:
          return data
        case TYPE.int:
          return Number(data)
        case TYPE.float:
          return parseFloat(data)
        case TYPE.bigint:
          return '' // ???
        case TYPE.array:
          return '' // ???
        case TYPE.object:
          return '' // ???
        case TYPE.symbol:
          return '' // ???
        case TYPE.undefined:
          return '' // ???
      }
    }

    try {
      return JSON.parse(value)
    } catch (e) {}

    return value
  }

  private toText(value: AcceptedValue): string {
    const type = typeof value

    if (value === null) {
      return `${TYPE.null}${this.separator}`
    } else if (type === 'boolean') {
      return `${TYPE.boolean}${this.separator}${value ? 'true' : 'false'}`
    } else if (type === 'string') {
      return `${TYPE.string}${this.separator}${value}`
    } else if (type === 'number') {
      if (Number.isInteger(value)) {
        return `${TYPE.int}${this.separator}${value}`
      } else {
        return `${TYPE.float}${this.separator}${value}`
      }
    } else if (Array.isArray(value) || type === 'object') {
      return JSON.stringify(value)
    }

    return ''
  }

  private cipher(str: string, key: string = ''): string {
    if (!str || !key)
      return ''

    const strBuffer = Buffer.from(str, this.encoding)
    const keyBuffer = Buffer.from(key, this.encoding)
    const result = Buffer.alloc(strBuffer.length)

    const dataLength = strBuffer.length - 1
    const keyLength = keyBuffer.length - 1

    for (let i = dataLength; i >= 0; i--)
      result[dataLength - i] = strBuffer[i] ^ keyBuffer[i % (keyLength + 1)]

    return result.toString(this.encoding)
  }

  private maybeUseAlphabet(data: string, from?: string, to?: string): string {
    if (!data || typeof data !== 'string' || !from || !to || from === to)
      return data

    return this.strtr(data, from, to)
  }

  private strtr(str: string, from: string, to: string): string {
    let result = ''

    for (let i = 0; i < str.length; i++) {
      const char = str[i]
      const index = from.indexOf(char)

      if (index !== -1 && index < to.length) {
        result += to[index]
      } else {
        result += char
      }
    }

    return result
  }

  private forgeKey(secret: string, entropy: string = ''): string {
    if (!entropy.trim())
      return secret

    return sha256(secret + entropy, 'binary').toString()
  }

  public baseEncode(data: string | Buffer): string {
    return this.maybeUseAlphabet(
      UrlSafeBase64.encode(data),
      this.config.baseAlphabet,
      this.config.alphabet
    )
  }

  public baseDecode(data: string): string {
    return UrlSafeBase64.decode(this.maybeUseAlphabet(
      data,
      this.config.alphabet,
      this.config.baseAlphabet
    ))
  }
}
