const {
  buildExternalTableId,
  breakExternalTableId,
} = require("../../../integrations/utils")
const {
  getTable,
  generateForeignKey,
  generateJunctionTableName,
  foreignKeyStructure,
  hasTypeChanged,
} = require("./utils")
const {
  DataSourceOperation,
  FieldTypes,
  RelationshipTypes,
} = require("../../../constants")
const { makeExternalQuery } = require("../../../integrations/base/utils")
const { cloneDeep } = require("lodash/fp")
const csvParser = require("../../../utilities/csvParser")
const { handleRequest } = require("../row/external")
const { getAppDB } = require("@devlego/backend-core/context")
const { events } = require("@devlego/backend-core")

async function makeTableRequest(
  datasource,
  operation,
  table,
  tables,
  oldTable = null,
  renamed = null
) {
  const json = {
    endpoint: {
      datasourceId: datasource._id,
      entityId: table._id,
      operation,
    },
    meta: {
      tables,
    },
    table,
  }
  if (oldTable) {
    json.meta.table = oldTable
  }
  if (renamed) {
    json.meta.renamed = renamed
  }
  return makeExternalQuery(datasource, json)
}

function cleanupRelationships(table, tables, oldTable = null) {
  const tableToIterate = oldTable ? oldTable : table
  // clean up relationships in couch table schemas
  for (let [key, schema] of Object.entries(tableToIterate.schema)) {
    if (
      schema.type === FieldTypes.LINK &&
      (!oldTable || table.schema[key] == null)
    ) {
      const relatedTable = Object.values(tables).find(
        table => table._id === schema.tableId
      )
      const foreignKey = schema.foreignKey
      if (!relatedTable || !foreignKey) {
        continue
      }
      for (let [relatedKey, relatedSchema] of Object.entries(
        relatedTable.schema
      )) {
        if (
          relatedSchema.type === FieldTypes.LINK &&
          relatedSchema.fieldName === foreignKey
        ) {
          delete relatedTable.schema[relatedKey]
        }
      }
    }
  }
}

function getDatasourceId(table) {
  if (!table) {
    throw "No table supplied"
  }
  if (table.sourceId) {
    return table.sourceId
  }
  return breakExternalTableId(table._id).datasourceId
}

function otherRelationshipType(type) {
  if (type === RelationshipTypes.MANY_TO_MANY) {
    return RelationshipTypes.MANY_TO_MANY
  }
  return type === RelationshipTypes.ONE_TO_MANY
    ? RelationshipTypes.MANY_TO_ONE
    : RelationshipTypes.ONE_TO_MANY
}

function generateManyLinkSchema(datasource, column, table, relatedTable) {
  const primary = table.name + table.primary[0]
  const relatedPrimary = relatedTable.name + relatedTable.primary[0]
  const jcTblName = generateJunctionTableName(column, table, relatedTable)
  // first create the new table
  const junctionTable = {
    _id: buildExternalTableId(datasource._id, jcTblName),
    name: jcTblName,
    primary: [primary, relatedPrimary],
    constrained: [primary, relatedPrimary],
    schema: {
      [primary]: foreignKeyStructure(primary, {
        toTable: table.name,
        toKey: table.primary[0],
      }),
      [relatedPrimary]: foreignKeyStructure(relatedPrimary, {
        toTable: relatedTable.name,
        toKey: relatedTable.primary[0],
      }),
    },
  }
  column.through = junctionTable._id
  column.throughFrom = relatedPrimary
  column.throughTo = primary
  column.fieldName = relatedPrimary
  return junctionTable
}

function generateLinkSchema(column, table, relatedTable, type) {
  const isOneSide = type === RelationshipTypes.ONE_TO_MANY
  const primary = isOneSide ? relatedTable.primary[0] : table.primary[0]
  // generate a foreign key
  const foreignKey = generateForeignKey(column, relatedTable)
  column.relationshipType = type
  column.foreignKey = isOneSide ? foreignKey : primary
  column.fieldName = isOneSide ? primary : foreignKey
  return foreignKey
}

function generateRelatedSchema(linkColumn, table, relatedTable, columnName) {
  // generate column for other table
  const relatedSchema = cloneDeep(linkColumn)
  // swap them from the main link
  if (linkColumn.foreignKey) {
    relatedSchema.fieldName = linkColumn.foreignKey
    relatedSchema.foreignKey = linkColumn.fieldName
  }
  // is many to many
  else {
    // don't need to copy through, already got it
    relatedSchema.fieldName = linkColumn.throughTo
    relatedSchema.throughTo = linkColumn.throughFrom
    relatedSchema.throughFrom = linkColumn.throughTo
  }
  relatedSchema.relationshipType = otherRelationshipType(
    linkColumn.relationshipType
  )
  relatedSchema.tableId = relatedTable._id
  relatedSchema.name = columnName
  table.schema[columnName] = relatedSchema
}

function isRelationshipSetup(column) {
  return column.foreignKey || column.through
}

exports.save = async function (ctx) {
  const table = ctx.request.body
  const { _rename: renamed } = table
  // can't do this right now
  delete table.dataImport
  const datasourceId = getDatasourceId(ctx.request.body)
  // table doesn't exist already, note that it is created
  if (!table._id) {
    table.created = true
  }
  let tableToSave = {
    type: "table",
    _id: buildExternalTableId(datasourceId, table.name),
    ...table,
  }

  let oldTable
  if (ctx.request.body && ctx.request.body._id) {
    oldTable = await getTable(ctx.request.body._id)
  }

  if (hasTypeChanged(tableToSave, oldTable)) {
    ctx.throw(400, "A column type has changed.")
  }

  const db = getAppDB()
  const datasource = await db.get(datasourceId)
  const oldTables = cloneDeep(datasource.entities)
  const tables = datasource.entities

  const extraTablesToUpdate = []

  // check if relations need setup
  for (let schema of Object.values(tableToSave.schema)) {
    if (schema.type !== FieldTypes.LINK || isRelationshipSetup(schema)) {
      continue
    }
    const relatedTable = Object.values(tables).find(
      table => table._id === schema.tableId
    )
    const relatedColumnName = schema.fieldName
    const relationType = schema.relationshipType
    if (relationType === RelationshipTypes.MANY_TO_MANY) {
      const junctionTable = generateManyLinkSchema(
        datasource,
        schema,
        table,
        relatedTable
      )
      if (tables[junctionTable.name]) {
        throw "Junction table already exists, cannot create another relationship."
      }
      tables[junctionTable.name] = junctionTable
      extraTablesToUpdate.push(junctionTable)
    } else {
      const fkTable =
        relationType === RelationshipTypes.ONE_TO_MANY ? table : relatedTable
      const foreignKey = generateLinkSchema(
        schema,
        table,
        relatedTable,
        relationType
      )
      fkTable.schema[foreignKey] = foreignKeyStructure(foreignKey)
      if (fkTable.constrained == null) {
        fkTable.constrained = []
      }
      if (fkTable.constrained.indexOf(foreignKey) === -1) {
        fkTable.constrained.push(foreignKey)
      }
      // foreign key is in other table, need to save it to external
      if (fkTable._id !== table._id) {
        extraTablesToUpdate.push(fkTable)
      }
    }
    generateRelatedSchema(schema, relatedTable, table, relatedColumnName)
    schema.main = true
  }

  cleanupRelationships(tableToSave, tables, oldTable)

  const operation = oldTable
    ? DataSourceOperation.UPDATE_TABLE
    : DataSourceOperation.CREATE_TABLE
  await makeTableRequest(
    datasource,
    operation,
    tableToSave,
    tables,
    oldTable,
    renamed
  )
  // update any extra tables (like foreign keys in other tables)
  for (let extraTable of extraTablesToUpdate) {
    const oldExtraTable = oldTables[extraTable.name]
    let op = oldExtraTable
      ? DataSourceOperation.UPDATE_TABLE
      : DataSourceOperation.CREATE_TABLE
    await makeTableRequest(datasource, op, extraTable, tables, oldExtraTable)
  }

  // make sure the constrained list, all still exist
  if (Array.isArray(tableToSave.constrained)) {
    tableToSave.constrained = tableToSave.constrained.filter(constraint =>
      Object.keys(tableToSave.schema).includes(constraint)
    )
  }

  // remove the rename prop
  delete tableToSave._rename
  // store it into couch now for budibase reference
  datasource.entities[tableToSave.name] = tableToSave
  await db.put(datasource)

  return tableToSave
}

exports.destroy = async function (ctx) {
  const tableToDelete = await getTable(ctx.params.tableId)
  if (!tableToDelete || !tableToDelete.created) {
    ctx.throw(400, "Cannot delete tables which weren't created in Budibase.")
  }
  const datasourceId = getDatasourceId(tableToDelete)

  const db = getAppDB()
  const datasource = await db.get(datasourceId)
  const tables = datasource.entities

  const operation = DataSourceOperation.DELETE_TABLE
  await makeTableRequest(datasource, operation, tableToDelete, tables)

  cleanupRelationships(tableToDelete, tables)

  delete datasource.entities[tableToDelete.name]
  await db.put(datasource)

  return tableToDelete
}

exports.bulkImport = async function (ctx) {
  const table = await getTable(ctx.params.tableId)
  const { dataImport } = ctx.request.body
  if (!dataImport || !dataImport.schema || !dataImport.csvString) {
    ctx.throw(400, "Provided data import information is invalid.")
  }
  const rows = await csvParser.transform({
    ...dataImport,
    existingTable: table,
  })
  await handleRequest(DataSourceOperation.BULK_CREATE, table._id, {
    rows,
  })
  await events.rows.imported(table, "csv", rows.length)
  return table
}
