const NODE_WIDTH = 260
const HEADER_HEIGHT = 60
const FIELD_HEIGHT = 28
const NODE_GAP_X = 80
const NODE_GAP_Y = 72
const MIN_NODE_HEIGHT = 132

function flagOn(value) {
  return value === true || value === 1 || value === '1' || value === 'true'
}

function cleanName(value, fallback) {
  const text = String(value || '').trim()
  return text || fallback
}

function normalizeKey(value) {
  return String(value || '')
    .toLowerCase()
    .replace(/[^a-z0-9]/g, '')
}

function tableIdOf(table, index) {
  return cleanName(table?.tableName || table?.id || table?.tableId, `table_${index + 1}`)
}

function buildField(column, index) {
  return {
    id: cleanName(column?.columnName || column?.id || column?.columnId, `field_${index + 1}`),
    name: cleanName(column?.columnName, `field_${index + 1}`),
    comment: String(column?.columnComment || '').trim(),
    type: String(column?.columnType || '').trim(),
    primary: flagOn(column?.isPk),
    required: flagOn(column?.isRequired)
  }
}

function buildNode(table, index, columns) {
  const fields = columns.map(buildField)
  const x = (index % 3) * (NODE_WIDTH + NODE_GAP_X)
  const y = Math.floor(index / 3) * (MIN_NODE_HEIGHT + NODE_GAP_Y)
  const height = Math.max(MIN_NODE_HEIGHT, HEADER_HEIGHT + fields.length * FIELD_HEIGHT + 18)

  return {
    id: tableIdOf(table, index),
    title: cleanName(table?.tableName, `table_${index + 1}`),
    comment: String(table?.tableComment || '').trim(),
    x,
    y,
    width: NODE_WIDTH,
    height,
    fields
  }
}

function savedPositionFor(draft, nodeId) {
  const position = draft?.positions?.[nodeId]
  const x = Number(position?.x)
  const y = Number(position?.y)
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
    return null
  }
  return { x, y }
}

function targetCandidates(base) {
  return [base, `${base}_info`, `${base}_table`, `${base}s`].map(normalizeKey)
}

function findTargetNode(nodes, base, sourceId) {
  const candidates = targetCandidates(base)
  const exact = nodes.find((node) => node.id !== sourceId && candidates.includes(normalizeKey(node.title)))
  if (exact) return exact

  return nodes.find((node) => {
    if (node.id === sourceId) return false
    const nodeKey = normalizeKey(node.title)
    return nodeKey.startsWith(normalizeKey(`${base}_`)) || nodeKey.startsWith(normalizeKey(base))
  })
}

function primaryFieldOf(node) {
  return node.fields.find((field) => field.primary) || node.fields.find((field) => field.name === 'id') || node.fields[0]
}

function buildInferredEdges(nodes, deletedEdgeIds) {
  const edges = []
  const deleted = new Set(Array.isArray(deletedEdgeIds) ? deletedEdgeIds : [])

  nodes.forEach((sourceNode) => {
    sourceNode.fields.forEach((field) => {
      const fieldName = String(field.name || '').toLowerCase()
      if (field.primary || !fieldName.endsWith('_id')) return

      const base = fieldName.slice(0, -3)
      if (!base) return

      const targetNode = findTargetNode(nodes, base, sourceNode.id)
      if (!targetNode) return

      const targetField = primaryFieldOf(targetNode)
      const id = `${sourceNode.id}.${field.name}->${targetNode.id}.${targetField?.name || 'id'}`
      if (deleted.has(id)) return

      edges.push({
        id,
        source: sourceNode.id,
        target: targetNode.id,
        fieldName: field.name,
        targetFieldName: targetField?.name || 'id',
        inferred: true
      })
    })
  })

  return edges
}

function buildManualEdges(nodes, relations) {
  const nodeIds = new Set(nodes.map((node) => node.id))
  return (Array.isArray(relations) ? relations : [])
    .filter((relation) => nodeIds.has(relation?.source) && nodeIds.has(relation?.target))
    .map((relation, index) => ({
      id: cleanName(relation.id, `manual_${index + 1}`),
      source: relation.source,
      target: relation.target,
      fieldName: cleanName(relation.fieldName, 'id'),
      targetFieldName: cleanName(relation.targetFieldName, 'id'),
      inferred: false
    }))
}

function applyPositions(nodes, draft) {
  return nodes.map((node) => {
    const position = savedPositionFor(draft, node.id)
    return position ? { ...node, ...position } : node
  })
}

export function buildErGraph(tables, draft = {}) {
  const sourceTables = Array.isArray(tables) ? tables : []
  const nodes = applyPositions(
    sourceTables.map((table, index) => buildNode(table, index, Array.isArray(table?.columns) ? table.columns : [])),
    draft
  )

  return {
    nodes,
    edges: [
      ...buildInferredEdges(nodes, draft?.deletedEdgeIds),
      ...buildManualEdges(nodes, draft?.relations)
    ]
  }
}
