import { SourceNames, SqlQuery } from "../definitions/datasource"
import { Datasource, Table } from "../definitions/common"
import { DocumentTypes, SEPARATOR } from "../db/utils"
import { FieldTypes, BuildSchemaErrors, InvalidColumns } from "../constants"

const DOUBLE_SEPARATOR = `${SEPARATOR}${SEPARATOR}`
const ROW_ID_REGEX = /^\[.*]$/g

const SQL_NUMBER_TYPE_MAP = {
  integer: FieldTypes.NUMBER,
  int: FieldTypes.NUMBER,
  bigint: FieldTypes.NUMBER,
  decimal: FieldTypes.NUMBER,
  smallint: FieldTypes.NUMBER,
  real: FieldTypes.NUMBER,
  float: FieldTypes.NUMBER,
  numeric: FieldTypes.NUMBER,
  mediumint: FieldTypes.NUMBER,
  dec: FieldTypes.NUMBER,
  double: FieldTypes.NUMBER,
  fixed: FieldTypes.NUMBER,
  "double precision": FieldTypes.NUMBER,
  number: FieldTypes.NUMBER,
  binary_float: FieldTypes.NUMBER,
  binary_double: FieldTypes.NUMBER,
  money: FieldTypes.NUMBER,
  smallmoney: FieldTypes.NUMBER,
}

const SQL_DATE_TYPE_MAP = {
  timestamp: FieldTypes.DATETIME,
  time: FieldTypes.DATETIME,
  datetime: FieldTypes.DATETIME,
  smalldatetime: FieldTypes.DATETIME,
  date: FieldTypes.DATETIME,
}

const SQL_STRING_TYPE_MAP = {
  varchar: FieldTypes.STRING,
  char: FieldTypes.STRING,
  nchar: FieldTypes.STRING,
  nvarchar: FieldTypes.STRING,
  ntext: FieldTypes.STRING,
  enum: FieldTypes.STRING,
  blob: FieldTypes.LONGFORM,
  long: FieldTypes.LONGFORM,
  text: FieldTypes.LONGFORM,
}

const SQL_BOOLEAN_TYPE_MAP = {
  boolean: FieldTypes.BOOLEAN,
  bit: FieldTypes.BOOLEAN,
  tinyint: FieldTypes.BOOLEAN,
}

const SQL_MISC_TYPE_MAP = {
  json: FieldTypes.JSON,
}

const SQL_TYPE_MAP = {
  ...SQL_NUMBER_TYPE_MAP,
  ...SQL_DATE_TYPE_MAP,
  ...SQL_STRING_TYPE_MAP,
  ...SQL_BOOLEAN_TYPE_MAP,
  ...SQL_MISC_TYPE_MAP,
}

export enum SqlClients {
  MS_SQL = "mssql",
  POSTGRES = "pg",
  MY_SQL = "mysql2",
  ORACLE = "oracledb",
}

export function isExternalTable(tableId: string) {
  return tableId.includes(DocumentTypes.DATASOURCE)
}

export function buildExternalTableId(datasourceId: string, tableName: string) {
  return `${datasourceId}${DOUBLE_SEPARATOR}${tableName}`
}

export function breakExternalTableId(tableId: string | undefined) {
  if (!tableId) {
    return {}
  }
  const parts = tableId.split(DOUBLE_SEPARATOR)
  let tableName = parts.pop()
  // if they need joined
  let datasourceId = parts.join(DOUBLE_SEPARATOR)
  return { datasourceId, tableName }
}

export function generateRowIdField(keyProps: any[] = []) {
  if (!Array.isArray(keyProps)) {
    keyProps = [keyProps]
  }
  // this conserves order and types
  // we have to swap the double quotes to single quotes for use in HBS statements
  // when using the literal helper the double quotes can break things
  return encodeURIComponent(JSON.stringify(keyProps).replace(/"/g, "'"))
}

export function isRowId(field: any) {
  return (
    Array.isArray(field) ||
    (typeof field === "string" && field.match(ROW_ID_REGEX) != null)
  )
}

export function convertRowId(field: any) {
  if (Array.isArray(field)) {
    return field[0]
  }
  if (typeof field === "string" && field.match(ROW_ID_REGEX) != null) {
    return field.substring(1, field.length - 1)
  }
  return field
}

// should always return an array
export function breakRowIdField(_id: string | { _id: string }): any[] {
  if (!_id) {
    return []
  }
  // have to replace on the way back as we swapped out the double quotes
  // when encoding, but JSON can't handle the single quotes
  const id = typeof _id === "string" ? _id : _id._id
  const decoded: string = decodeURIComponent(id).replace(/'/g, '"')
  try {
    const parsed = JSON.parse(decoded)
    return Array.isArray(parsed) ? parsed : [parsed]
  } catch (err) {
    // wasn't json - likely was handlebars for a many to many
    return [_id]
  }
}

export function convertSqlType(type: string) {
  for (let [external, internal] of Object.entries(SQL_TYPE_MAP)) {
    if (type.toLowerCase().includes(external)) {
      return internal
    }
  }
  return FieldTypes.STRING
}

export function getSqlQuery(query: SqlQuery | string): SqlQuery {
  if (typeof query === "string") {
    return { sql: query }
  } else {
    return query
  }
}

export function isSQL(datasource: Datasource): boolean {
  if (!datasource || !datasource.source) {
    return false
  }
  const SQL = [
    SourceNames.POSTGRES,
    SourceNames.SQL_SERVER,
    SourceNames.MYSQL,
    SourceNames.ORACLE,
  ]
  return SQL.indexOf(datasource.source) !== -1
}

export function isIsoDateString(str: string) {
  if (!/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z/.test(str)) {
    return false
  }
  let d = new Date(str)
  return d.toISOString() === str
}

/**
 * This function will determine whether a column is a relationship and whether it
 * is currently valid. The reason for the validity check is that tables can be deleted
 * outside of Budibase control and if this is the case it will break Budibase relationships.
 * The tableIds is a list passed down from the main finalise tables function, which is
 * based on the tables that have just been fetched. This will only really be used on subsequent
 * fetches to the first one - if the user is periodically refreshing Budibase knowledge of tables.
 * @param column The column to check, to see if it is a valid relationship.
 * @param tableIds The IDs of the tables which currently exist.
 */
function shouldCopyRelationship(
  column: { type: string; tableId?: string },
  tableIds: [string]
) {
  return (
    column.type === FieldTypes.LINK &&
    column.tableId &&
    tableIds.includes(column.tableId)
  )
}

/**
 * Similar function to the shouldCopyRelationship function, but instead this looks for options and boolean
 * types. It is possible to switch a string -> options and a number -> boolean (and vice versus) need to make
 * sure that these get copied over when tables are fetched. Also checks whether they are still valid, if a
 * column has changed type in the external database then copying it over may not be possible.
 * @param column The column to check for options or boolean type.
 * @param fetchedColumn The fetched column to check for the type in the external database.
 */
function shouldCopySpecialColumn(
  column: { type: string },
  fetchedColumn: { type: string } | undefined
) {
  return (
    column.type === FieldTypes.OPTIONS ||
    column.type === FieldTypes.ARRAY ||
    ((!fetchedColumn || fetchedColumn.type === FieldTypes.NUMBER) &&
      column.type === FieldTypes.BOOLEAN)
  )
}

/**
 * Looks for columns which need to be copied over into the new table definitions, like relationships
 * and options types.
 * @param tableName The name of the table which is being checked.
 * @param table The specific table which is being checked.
 * @param entities All the tables that existed before - the old table definitions.
 * @param tableIds The IDs of the tables which exist now, to check if anything has been removed.
 */
function copyExistingPropsOver(
  tableName: string,
  table: Table,
  entities: { [key: string]: any },
  tableIds: [string]
) {
  if (entities && entities[tableName]) {
    if (entities[tableName].primaryDisplay) {
      table.primaryDisplay = entities[tableName].primaryDisplay
    }
    const existingTableSchema = entities[tableName].schema
    for (let key in existingTableSchema) {
      if (!existingTableSchema.hasOwnProperty(key)) {
        continue
      }
      const column = existingTableSchema[key]
      if (
        shouldCopyRelationship(column, tableIds) ||
        shouldCopySpecialColumn(column, table.schema[key])
      ) {
        table.schema[key] = existingTableSchema[key]
      }
    }
  }
  return table
}

/**
 * Look through the final table definitions to see if anything needs to be
 * copied over from the old and if any errors have occurred mark them so
 * that the user can be made aware.
 * @param tables The list of tables that have been retrieved from the external database.
 * @param entities The old list of tables, if there was any to look for definitions in.
 */
export function finaliseExternalTables(
  tables: { [key: string]: any },
  entities: { [key: string]: any }
) {
  const invalidColumns = Object.values(InvalidColumns)
  let finalTables: { [key: string]: any } = {}
  const errors: { [key: string]: string } = {}
  // @ts-ignore
  const tableIds: [string] = Object.values(tables).map(table => table._id)
  for (let [name, table] of Object.entries(tables)) {
    const schemaFields = Object.keys(table.schema)
    // make sure every table has a key
    if (table.primary == null || table.primary.length === 0) {
      errors[name] = BuildSchemaErrors.NO_KEY
      continue
    } else if (schemaFields.find(field => invalidColumns.includes(field))) {
      errors[name] = BuildSchemaErrors.INVALID_COLUMN
      continue
    }
    // make sure all previous props have been added back
    finalTables[name] = copyExistingPropsOver(name, table, entities, tableIds)
  }
  // sort the tables by name
  finalTables = Object.entries(finalTables)
    .sort(([a], [b]) => a.localeCompare(b))
    .reduce((r, [k, v]) => ({ ...r, [k]: v }), {})
  return { tables: finalTables, errors }
}
