const SOURCE_X = 32
const FRONTEND_X = 360
const ADMIN_X = 688
const START_Y = 72
const NODE_WIDTH = 252
const NODE_HEIGHT = 108
const NODE_GAP_Y = 24

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

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

function normalizeKey(value, fallback) {
  const text = cleanText(value, fallback)
    .toLowerCase()
    .replace(/[^a-z0-9_]+/g, '_')
    .replace(/_+/g, '_')
    .replace(/^_+|_+$/g, '')
  return text || fallback
}

function uniqueKey(value, fallbackPrefix, index, used) {
  const base = normalizeKey(value, `${fallbackPrefix}_${index + 1}`)
  let candidate = base
  let suffix = 2
  while (used.has(candidate)) {
    candidate = `${base}_${suffix}`
    suffix += 1
  }
  used.add(candidate)
  return candidate
}

function normalizeRoles(roles) {
  const used = new Set()
  return (Array.isArray(roles) ? roles : []).filter(Boolean).map((role, index) => {
    const code = uniqueKey(role.code, 'role', index, used)
    return {
      code,
      id: `role:${code}`,
      title: cleanText(role.name, code),
      subtitle: cleanText(role.description, '角色'),
      kind: 'role',
      lane: 'role',
      badges: ['角色']
    }
  })
}

function normalizeMenus(menus, lane, fallbackPrefix) {
  const used = new Set()
  return (Array.isArray(menus) ? menus : []).filter(Boolean).map((menu, index) => {
    const code = uniqueKey(menu.code, fallbackPrefix, index, used)
    const requiresLogin = flagOn(menu.requiresLogin)
    const dataScope = cleanText(menu.dataScope, requiresLogin ? 'CURRENT_USER' : 'PUBLIC').toUpperCase()
    const visibleRoles = Array.isArray(menu.visibleRoles)
      ? menu.visibleRoles.map((role) => normalizeKey(role, '')).filter(Boolean)
      : []

    return {
      code,
      id: `${lane}:${code}`,
      title: cleanText(menu.name, code),
      subtitle: cleanText(menu.path, '/'),
      kind: 'menu',
      lane,
      requiresLogin,
      visibleRoles,
      dataScope,
      badges: [
        lane === 'frontend' ? '前台' : '后台',
        requiresLogin ? '需登录' : '公开',
        dataScope
      ]
    }
  })
}

function shouldUsePublic(menu) {
  return !menu.requiresLogin || menu.dataScope === 'PUBLIC'
}

function needsPublicAccess(menus) {
  return menus.some(shouldUsePublic)
}

function needsLoggedInAccess(menus, roleIds) {
  return menus.some((menu) => {
    if (!menu.requiresLogin) return false
    return !menu.visibleRoles.some((roleCode) => roleIds.has(`role:${roleCode}`))
  })
}

function buildAccessNodes(menus, roleIds) {
  const nodes = []
  if (needsPublicAccess(menus)) {
    nodes.push({
      code: 'public',
      id: 'access:public',
      title: '公开访问',
      subtitle: '无需登录',
      kind: 'access',
      lane: 'access',
      badges: ['PUBLIC']
    })
  }
  if (needsLoggedInAccess(menus, roleIds)) {
    nodes.push({
      code: 'logged_in',
      id: 'access:logged_in',
      title: '登录用户',
      subtitle: '未限定角色',
      kind: 'access',
      lane: 'access',
      badges: ['LOGIN']
    })
  }
  return nodes
}

function applyLayout(nodes) {
  const laneIndexes = new Map()
  return nodes.map((node) => {
    const layoutLane = node.lane === 'admin' ? 'admin' : node.lane === 'frontend' ? 'frontend' : 'source'
    const index = laneIndexes.get(layoutLane) || 0
    laneIndexes.set(layoutLane, index + 1)
    const x = layoutLane === 'admin' ? ADMIN_X : layoutLane === 'frontend' ? FRONTEND_X : SOURCE_X

    return {
      ...node,
      x,
      y: START_Y + index * (NODE_HEIGHT + NODE_GAP_Y),
      width: NODE_WIDTH,
      height: NODE_HEIGHT
    }
  })
}

function edgeLabel(menu) {
  return menu.dataScope || (menu.requiresLogin ? 'LOGIN' : 'PUBLIC')
}

function edgeId(source, target) {
  return `${source}->${target}`
}

function buildEdges(menus, roleIds) {
  const edges = []
  menus.forEach((menu) => {
    const validRoleIds = menu.visibleRoles
      .map((roleCode) => `role:${roleCode}`)
      .filter((roleId) => roleIds.has(roleId))

    if (validRoleIds.length) {
      validRoleIds.forEach((roleId) => {
        edges.push({
          id: edgeId(roleId, menu.id),
          source: roleId,
          target: menu.id,
          label: edgeLabel(menu)
        })
      })
      return
    }

    const source = shouldUsePublic(menu) ? 'access:public' : 'access:logged_in'
    edges.push({
      id: edgeId(source, menu.id),
      source,
      target: menu.id,
      label: edgeLabel(menu)
    })
  })
  return edges
}

export function buildAppModuleGraph(blueprint = {}) {
  const roles = normalizeRoles(blueprint.roles)
  const frontendMenus = normalizeMenus(blueprint.frontendMenus, 'frontend', 'frontend_menu')
  const adminMenus = normalizeMenus(blueprint.adminMenus, 'admin', 'admin_menu')
  const menus = [...frontendMenus, ...adminMenus]
  const roleIds = new Set(roles.map((role) => role.id))
  const accessNodes = buildAccessNodes(menus, roleIds)
  const nodes = applyLayout([...accessNodes, ...roles, ...frontendMenus, ...adminMenus])

  return {
    nodes,
    edges: buildEdges(menus, roleIds),
    lanes: [
      { key: 'source', title: '访问角色', x: SOURCE_X, width: NODE_WIDTH },
      { key: 'frontend', title: '前台功能模块', x: FRONTEND_X, width: NODE_WIDTH },
      { key: 'admin', title: '后台功能模块', x: ADMIN_X, width: NODE_WIDTH }
    ]
  }
}
