function escapeXml(value) {
  return String(value ?? '')
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;')
}

function verticalChars(value) {
  const chars = Array.from(String(value ?? '').trim())
  if (chars.length <= 7) return chars
  return [...chars.slice(0, 6), '…']
}

function verticalTextY(node) {
  const count = verticalChars(node.title).length
  return Math.max(22, (node.height - (count - 1) * 18) / 2 + 5)
}

function shortText(value, maxLength) {
  const text = String(value ?? '')
  return text.length > maxLength ? `${text.slice(0, maxLength - 3)}...` : text
}

function nodeFontSize(node, fallback) {
  return Number(node.fontSize) || fallback
}

function renderVerticalText(node) {
  const x = node.width / 2
  const startY = verticalTextY(node)
  return `
      <text class="module-label" x="${x}" y="${startY}" font-size="${nodeFontSize(node, 16)}">
        ${verticalChars(node.title).map((char, index) => `<tspan x="${x}" dy="${index === 0 ? 0 : 18}">${escapeXml(char)}</tspan>`).join('')}
      </text>`
}

function renderHorizontalText(node) {
  return `
      <text class="node-title" x="${node.width / 2}" y="${node.height / 2}" font-size="${nodeFontSize(node, node.kind === 'root' ? 17 : 16)}">${escapeXml(shortText(node.title, 18))}</text>`
}

function renderNode(node) {
  const className = ['module-node', node.kind, node.lane].filter(Boolean).join(' ')
  return `
    <g class="${escapeXml(className)}" transform="translate(${node.x}, ${node.y})">
      <rect class="module-node-box" width="${node.width}" height="${node.height}" fill="#ffffff" stroke="#222222" stroke-width="1" />
      ${node.vertical ? renderVerticalText(node) : renderHorizontalText(node)}
    </g>`
}

function renderEdge(edge) {
  return `
    <path class="tree-edge" d="${escapeXml(edge.path)}" fill="none" stroke="#222222" stroke-width="1.2" vector-effect="non-scaling-stroke" />`
}

export function buildAppModuleExportSvg(graph = {}) {
  const canvas = graph.canvas || {}
  const width = Math.max(1, Number(canvas.width) || 1)
  const height = Math.max(1, Number(canvas.height) || 1)
  const nodes = Array.isArray(graph.nodes) ? graph.nodes : []
  const edges = Array.isArray(graph.edges) ? graph.edges : []

  return `<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
  <style>
    .tree-edge{fill:none;stroke:#222;stroke-width:1.2;shape-rendering:crispEdges}
    .module-node-box{fill:#fff;stroke:#222;stroke-width:1;shape-rendering:crispEdges}
    .node-title{fill:#111;font-family:SimSun,"宋体","Microsoft YaHei",Arial,sans-serif;font-size:16px;font-weight:600;text-anchor:middle;dominant-baseline:middle}
    .module-node.root .node-title{font-size:17px}
    .module-label{fill:#111;font-family:SimSun,"宋体","Microsoft YaHei",Arial,sans-serif;font-size:16px;font-weight:600;text-anchor:middle}
    .figure-caption{fill:#111;font-family:SimSun,"宋体","Microsoft YaHei",Arial,sans-serif;font-size:14px;text-anchor:middle}
  </style>
  <rect width="100%" height="100%" fill="#ffffff" />
  <g>${edges.map(renderEdge).join('')}</g>
  <g>${nodes.map(renderNode).join('')}</g>
  <text class="figure-caption" x="${width / 2}" y="${height - 24}">${escapeXml(graph.caption)}</text>
</svg>`
}
