/**
 * Helpers to convert the change Payload into native JS types.
 */

// Adapted from epgsql (src/epgsql_binary.erl), this module licensed under
// 3-clause BSD found here: https://raw.githubusercontent.com/epgsql/epgsql/devel/LICENSE

export enum PostgresTypes {
  abstime = 'abstime',
  bool = 'bool',
  date = 'date',
  daterange = 'daterange',
  float4 = 'float4',
  float8 = 'float8',
  int2 = 'int2',
  int4 = 'int4',
  int4range = 'int4range',
  int8 = 'int8',
  int8range = 'int8range',
  json = 'json',
  jsonb = 'jsonb',
  money = 'money',
  numeric = 'numeric',
  oid = 'oid',
  reltime = 'reltime',
  text = 'text',
  time = 'time',
  timestamp = 'timestamp',
  timestamptz = 'timestamptz',
  timetz = 'timetz',
  tsrange = 'tsrange',
  tstzrange = 'tstzrange',
}

type Columns = {
  name: string // the column name. eg: "user_id"
  type: string // the column type. eg: "uuid"
  flags?: string[] // any special flags for the column. eg: ["key"]
  type_modifier?: number // the type modifier. eg: 4294967295
}[]

type BaseValue = null | string | number | boolean
type RecordValue = BaseValue | BaseValue[]

type Record = {
  [key: string]: RecordValue
}

/**
 * Takes an array of columns and an object of string values then converts each string value
 * to its mapped type.
 *
 * @param {{name: String, type: String}[]} columns
 * @param {Object} record
 * @param {Object} options The map of various options that can be applied to the mapper
 * @param {Array} options.skipTypes The array of types that should not be converted
 *
 * @example convertChangeData([{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age:'33'}, {})
 * //=>{ first_name: 'Paul', age: 33 }
 */
export const convertChangeData = (
  columns: Columns,
  record: Record | null,
  options: { skipTypes?: string[] } = {}
): Record => {
  const skipTypes = options.skipTypes ?? []

  if (!record) {
    return {}
  }

  return Object.keys(record).reduce((acc, rec_key) => {
    acc[rec_key] = convertColumn(rec_key, columns, record, skipTypes)
    return acc
  }, {} as Record)
}

/**
 * Converts the value of an individual column.
 *
 * @param {String} columnName The column that you want to convert
 * @param {{name: String, type: String}[]} columns All of the columns
 * @param {Object} record The map of string values
 * @param {Array} skipTypes An array of types that should not be converted
 * @return {object} Useless information
 *
 * @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, [])
 * //=> 33
 * @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, ['int4'])
 * //=> "33"
 */
export const convertColumn = (
  columnName: string,
  columns: Columns,
  record: Record,
  skipTypes: string[]
): RecordValue => {
  const column = columns.find((x) => x.name === columnName)
  const colType = column?.type
  const value = record[columnName]

  if (colType && !skipTypes.includes(colType)) {
    return convertCell(colType, value)
  }

  return noop(value)
}

/**
 * If the value of the cell is `null`, returns null.
 * Otherwise converts the string value to the correct type.
 * @param {String} type A postgres column type
 * @param {String} value The cell value
 *
 * @example convertCell('bool', 't')
 * //=> true
 * @example convertCell('int8', '10')
 * //=> 10
 * @example convertCell('_int4', '{1,2,3,4}')
 * //=> [1,2,3,4]
 */
export const convertCell = (type: string, value: RecordValue): RecordValue => {
  // if data type is an array
  if (type.charAt(0) === '_') {
    const dataType = type.slice(1, type.length)
    return toArray(value, dataType)
  }

  // If not null, convert to correct type.
  switch (type) {
    case PostgresTypes.bool:
      return toBoolean(value)
    case PostgresTypes.float4:
    case PostgresTypes.float8:
    case PostgresTypes.int2:
    case PostgresTypes.int4:
    case PostgresTypes.int8:
    case PostgresTypes.numeric:
    case PostgresTypes.oid:
      return toNumber(value)
    case PostgresTypes.json:
    case PostgresTypes.jsonb:
      return toJson(value)
    case PostgresTypes.timestamp:
      return toTimestampString(value) // Format to be consistent with PostgREST
    case PostgresTypes.abstime: // To allow users to cast it based on Timezone
    case PostgresTypes.date: // To allow users to cast it based on Timezone
    case PostgresTypes.daterange:
    case PostgresTypes.int4range:
    case PostgresTypes.int8range:
    case PostgresTypes.money:
    case PostgresTypes.reltime: // To allow users to cast it based on Timezone
    case PostgresTypes.text:
    case PostgresTypes.time: // To allow users to cast it based on Timezone
    case PostgresTypes.timestamptz: // To allow users to cast it based on Timezone
    case PostgresTypes.timetz: // To allow users to cast it based on Timezone
    case PostgresTypes.tsrange:
    case PostgresTypes.tstzrange:
      return noop(value)
    default:
      // Return the value for remaining types
      return noop(value)
  }
}

const noop = (value: RecordValue): RecordValue => {
  return value
}
export const toBoolean = (value: RecordValue): RecordValue => {
  switch (value) {
    case 't':
      return true
    case 'f':
      return false
    default:
      return value
  }
}
export const toNumber = (value: RecordValue): RecordValue => {
  if (typeof value === 'string') {
    const parsedValue = parseFloat(value)
    if (!Number.isNaN(parsedValue)) {
      return parsedValue
    }
  }
  return value
}
export const toJson = (value: RecordValue): RecordValue => {
  if (typeof value === 'string') {
    try {
      return JSON.parse(value)
    } catch {
      return value
    }
  }
  return value
}

/**
 * Converts a Postgres Array into a native JS array
 *
 * @example toArray('{}', 'int4')
 * //=> []
 * @example toArray('{"[2021-01-01,2021-12-31)","(2021-01-01,2021-12-32]"}', 'daterange')
 * //=> ['[2021-01-01,2021-12-31)', '(2021-01-01,2021-12-32]']
 * @example toArray([1,2,3,4], 'int4')
 * //=> [1,2,3,4]
 */
export const toArray = (value: RecordValue, type: string): RecordValue => {
  if (typeof value !== 'string') {
    return value
  }

  const lastIdx = value.length - 1
  const closeBrace = value[lastIdx]
  const openBrace = value[0]

  // Confirm value is a Postgres array by checking curly brackets
  if (openBrace === '{' && closeBrace === '}') {
    let arr
    const valTrim = value.slice(1, lastIdx)

    // TODO: find a better solution to separate Postgres array data
    try {
      arr = JSON.parse('[' + valTrim + ']')
    } catch (_) {
      // WARNING: splitting on comma does not cover all edge cases
      arr = valTrim ? valTrim.split(',') : []
    }

    return arr.map((val: BaseValue) => convertCell(type, val))
  }

  return value
}

/**
 * Fixes timestamp to be ISO-8601. Swaps the space between the date and time for a 'T'
 * See https://github.com/supabase/supabase/issues/18
 *
 * @example toTimestampString('2019-09-10 00:00:00')
 * //=> '2019-09-10T00:00:00'
 */
export const toTimestampString = (value: RecordValue): RecordValue => {
  if (typeof value === 'string') {
    return value.replace(' ', 'T')
  }

  return value
}

export const httpEndpointURL = (socketUrl: string): string => {
  const wsUrl = new URL(socketUrl)

  wsUrl.protocol = wsUrl.protocol.replace(/^ws/i, 'http')

  wsUrl.pathname = wsUrl.pathname
    .replace(/\/+$/, '') // remove all trailing slashes
    .replace(/\/socket\/websocket$/i, '') // remove the socket/websocket path
    .replace(/\/socket$/i, '') // remove the socket path
    .replace(/\/websocket$/i, '') // remove the websocket path

  if (wsUrl.pathname === '' || wsUrl.pathname === '/') {
    wsUrl.pathname = '/api/broadcast'
  } else {
    wsUrl.pathname = wsUrl.pathname + '/api/broadcast'
  }

  return wsUrl.href
}
