/**
 * Board Model - 白板数据模型层
 *
 * 职责：
 * - 管理自由画布上的元素（非树形）
 * - 支持多种元素类型
 * - 连线系统（箭头/连接器）
 * - 多页面管理
 * - 序列化/反序列化
 *
 * 设计原则：
 * - 纯数据操作，不涉及渲染
 * - 可观察的数据变更
 * - 支持撤销/重做
 * - Z-Index 排序
 */

import type {
  BoardElement, BoardElementBase, ElementType, ElementStyle,
  Connection, ConnectionStyle, AnchorPosition,
  BoardPage, SerializedBoard, Point,
  RectangleElement, EllipseElement, DiamondElement,
  TextElement, StickyNoteElement, ArrowElement,
  LineElement, FreehandElement, ImageElement, GroupElement,
} from './types'

import {
  SHAPE_DEFAULTS as SHAPE_DEFAULTS_VAL,
  ELEMENT_COLORS as ELEMENT_COLORS_VAL,
  DEFAULT_ELEMENT_STYLE as DEFAULT_STYLE_VAL,
  DEFAULT_CONNECTION_STYLE as DEFAULT_CONN_STYLE_VAL,
} from './types'

/** 生成唯一ID */
function generateId(prefix: string = 'el'): string {
  return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}

/** 生成页面ID */
function generatePageId(): string {
  return `page-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`
}

/**
 * 白板数据模型
 */
export class BoardModel {
  /** 所有元素 */
  private _elements: Map<string, BoardElement> = new Map()
  /** 连接 */
  private _connections: Map<string, Connection> = new Map()
  /** 页面 */
  private _pages: Map<string, BoardPage> = new Map()
  /** 当前活动页面ID */
  private _activePageId: string = ''
  /** Z-Index 计数器 */
  private _zIndexCounter: number = 0

  // 变更监听器
  private _changeListeners: Set<() => void> = new Set()

  /** 当前白板数据的 modified 时间戳（从服务器同步） */
  modified: string = ''

  constructor(data?: Partial<SerializedBoard>) {
    // 创建默认页面
    const defaultPageId = generatePageId()
    this._pages.set(defaultPageId, {
      id: defaultPageId,
      name: 'Page 1',
      elementIds: [],
    })
    this._activePageId = defaultPageId

    if (data) {
      if (data.elements) this.loadElements(data.elements)
      if (data.connections) this.loadConnections(data.connections)
      if (data.pages && data.pages.length > 0) {
        this._pages.clear()
        data.pages.forEach(p => this._pages.set(p.id, p))
      }
      if (data.activePageId && this._pages.has(data.activePageId)) {
        this._activePageId = data.activePageId
      }
    }
  }

  // ===== 元素查询 =====

  /** 获取所有元素 */
  get elements(): BoardElement[] {
    return Array.from(this._elements.values()).sort((a, b) => a.zIndex - b.zIndex)
  }

  /** 获取当前页面的元素 */
  get activePageElements(): BoardElement[] {
    return this.elements.filter(el => el.pageId === this._activePageId)
  }

  /** 获取元素 */
  getElement(id: string): BoardElement | undefined {
    return this._elements.get(id)
  }

  /** 根据位置查找最上层元素 */
  getElementAtPoint(point: Point, pageId?: string, zoom: number = 1): BoardElement | undefined {
    const elements = (pageId ? this.elements.filter(el => el.pageId === pageId) : this.elements)
    for (let i = elements.length - 1; i >= 0; i--) {
      const el = elements[i]
      if (this.isPointInElement(point, el, zoom)) {
        return el
      }
    }
    return undefined
  }

  /** 判断点是否在元素内 */
  private isPointInElement(point: Point, el: BoardElement, zoom: number): boolean {
    if (el.type === 'freehand') {
      const freehand = el as FreehandElement
      const hitRadius = 8 / zoom
      return freehand.points.some(p => {
        const dx = (p.x + el.x) - point.x
        const dy = (p.y + el.y) - point.y
        return dx * dx + dy * dy <= hitRadius * hitRadius
      })
    }
    if (el.type === 'arrow' || el.type === 'line') {
      const arrow = el as ArrowElement | LineElement
      const hitRadius = 6 / zoom
      const sp = 'startPoint' in arrow ? arrow.startPoint : { x: el.x, y: el.y }
      const ep = 'endPoint' in arrow ? arrow.endPoint : { x: el.x + el.width, y: el.y }
      const route = arrow.connectionStyle?.route || 'straight'
      if (route === 'bezier') {
        const { cp1, cp2 } = this.computeBezierControlPoints(sp, ep, arrow.controlPoints)
        return this.distToBezier(point, sp, cp1, cp2, ep) <= hitRadius
      }
      return this.distToSegment(point, sp, ep) <= hitRadius
    }
    // 矩形区域命中检测（含旋转）
    const cx = el.x + el.width / 2
    const cy = el.y + el.height / 2
    const rot = el.rotation ?? 0
    // 反旋转点到元素局部坐标
    const cos = Math.cos(-rot)
    const sin = Math.sin(-rot)
    const dx = point.x - cx
    const dy = point.y - cy
    const localX = dx * cos - dy * sin + el.width / 2
    const localY = dx * sin + dy * cos + el.height / 2

    if (false) {}

    if (el.type === 'ellipse') {
      const rx = el.width / 2
      const ry = el.height / 2
      return ((localX - rx) ** 2) / (rx ** 2) + ((localY - ry) ** 2) / (ry ** 2) <= 1
    }
    if (el.type === 'diamond') {
      const rx = el.width / 2
      const ry = el.height / 2
      return Math.abs(localX - rx) / rx + Math.abs(localY - ry) / ry <= 1
    }
    return localX >= 0 && localX <= el.width && localY >= 0 && localY <= el.height
  }

  /** 点到线段距离 */
  private distToSegment(p: Point, a: Point, b: Point): number {
    const dx = b.x - a.x
    const dy = b.y - a.y
    const lenSq = dx * dx + dy * dy
    if (lenSq === 0) return Math.hypot(p.x - a.x, p.y - a.y)
    let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / lenSq
    t = Math.max(0, Math.min(1, t))
    return Math.hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy))
  }

  /** 计算 Bezier 曲线上任意参数 t (0-1) 对应的点 */
  private getBezierPointAtT(t: number, p0: Point, p1: Point, p2: Point, p3: Point): Point {
    const u = 1 - t
    const uu = u * u
    const tt = t * t
    return {
      x: uu * u * p0.x + 3 * uu * t * p1.x + 3 * u * tt * p2.x + tt * t * p3.x,
      y: uu * u * p0.y + 3 * uu * t * p1.y + 3 * u * tt * p2.y + tt * t * p3.y,
    }
  }

  /** 计算点到 Bezier 曲线的最小距离 */
  private distToBezier(p: Point, p0: Point, p1: Point, p2: Point, p3: Point, samples: number = 50): number {
    let minDist = Infinity
    for (let i = 0; i <= samples; i++) {
      const t = i / samples
      const pt = this.getBezierPointAtT(t, p0, p1, p2, p3)
      const d = Math.hypot(p.x - pt.x, p.y - pt.y)
      if (d < minDist) minDist = d
    }
    return minDist
  }

  /** 计算 Bezier 控制点（S 形曲线，与 BoardRenderer 保持一致） */
  private computeBezierControlPoints(start: Point, end: Point, override?: { cp1: Point; cp2: Point }): { cp1: Point; cp2: Point } {
    if (override) return override
    const dx = end.x - start.x
    const dy = end.y - start.y
    const len = Math.hypot(dx, dy)
    if (len === 0) return { cp1: start, cp2: end }
    const perpX = -dy / len
    const perpY = dx / len
    const offset = len * 0.25
    return {
      cp1: { x: start.x + dx * 0.25 + perpX * offset, y: start.y + dy * 0.25 + perpY * offset },
      cp2: { x: end.x - dx * 0.25 + perpX * offset, y: end.y - dy * 0.25 + perpY * offset },
    }
  }

  /** 获取元素的贝塞尔控制点（优先使用存储值，否则自动计算） */
  getBezierControlPoints(el: ArrowElement | LineElement): { cp1: Point; cp2: Point } {
    const sp = el.startPoint
    const ep = el.endPoint
    const override = el.controlPoints
    return this.computeBezierControlPoints(sp, ep, override)
  }

  // ===== 元素操作 =====

  /** 复制元素（安全浅拷贝 + 生成新ID） */
  duplicateElement(id: string, offsetX = 10, offsetY = 10): BoardElement | undefined {
    const src = this._elements.get(id)
    if (!src) return undefined

    const base: BoardElementBase = {
      id: generateId('el'),
      type: src.type,
      x: src.x + offsetX,
      y: src.y + offsetY,
      width: src.width,
      height: src.height,
      rotation: src.rotation,
      style: { ...src.style },
      zIndex: this._zIndexCounter++,
      locked: src.locked,
      groupId: src.groupId,
      pageId: src.pageId,
    }

    let copy: BoardElement

    switch (src.type) {
      case 'rectangle':
        copy = { ...base, type: 'rectangle', text: (src as RectangleElement).text }
        break
      case 'ellipse':
        copy = { ...base, type: 'ellipse', text: (src as EllipseElement).text }
        break
      case 'diamond':
        copy = { ...base, type: 'diamond', text: (src as DiamondElement).text }
        break
      case 'text':
        copy = { ...base, type: 'text', text: (src as TextElement).text }
        break
      case 'sticky-note':
        copy = { ...base, type: 'sticky-note', text: (src as StickyNoteElement).text }
        break
      case 'arrow': {
        const s = src as ArrowElement
        copy = {
          ...base,
          type: 'arrow',
          connectionStyle: { ...s.connectionStyle },
          sourceId: s.sourceId,
          targetId: s.targetId,
          sourceAnchor: s.sourceAnchor,
          targetAnchor: s.targetAnchor,
          startPoint: { ...s.startPoint },
          endPoint: { ...s.endPoint },
          waypoints: s.waypoints?.map(p => ({ ...p })),
          controlPoints: s.controlPoints ? { cp1: { ...s.controlPoints.cp1 }, cp2: { ...s.controlPoints.cp2 } } : undefined,
          label: s.label,
        }
        break
      }
      case 'line': {
        const s = src as LineElement
        copy = {
          ...base,
          type: 'line',
          connectionStyle: { ...s.connectionStyle },
          startPoint: { ...s.startPoint },
          endPoint: { ...s.endPoint },
          controlPoints: s.controlPoints ? { cp1: { ...s.controlPoints.cp1 }, cp2: { ...s.controlPoints.cp2 } } : undefined,
        }
        break
      }
      case 'freehand':
        copy = { ...base, type: 'freehand', points: [...(src as FreehandElement).points], smoothing: (src as FreehandElement).smoothing }
        break
      case 'image':
        copy = { ...base, type: 'image', src: (src as ImageElement).src }
        break
      case 'group':
        copy = { ...base, type: 'group', childIds: [...(src as GroupElement).childIds] }
        break
      default:
        copy = { ...base, type: src.type }
    }

    this._elements.set(copy.id, copy)
    this._notifyChange()
    return copy
  }

  /** 添加元素 */
  addElement(type: ElementType, options: Partial<BoardElementBase> = {}): BoardElement {
    const id = options.id || generateId('el')
    const defaults = SHAPE_DEFAULTS_VAL[type] || SHAPE_DEFAULTS_VAL.rectangle
    const colors = ELEMENT_COLORS_VAL[type] || ELEMENT_COLORS_VAL.rectangle

    const base: BoardElementBase = {
      id,
      type,
      x: options.x ?? 100,
      y: options.y ?? 100,
      width: options.width ?? defaults.width,
      height: options.height ?? defaults.height,
      rotation: options.rotation ?? 0,
      style: {
        ...DEFAULT_STYLE_VAL,
        ...colors,
        ...(options.style || {}),
      },
      zIndex: options.zIndex ?? this._zIndexCounter++,
      locked: options.locked ?? false,
      groupId: options.groupId ?? null,
      pageId: options.pageId ?? this._activePageId,
    }

    let element: BoardElement

    switch (type) {
      case 'rectangle':
        element = { ...base, type: 'rectangle', text: (options as any).text } as RectangleElement
        break
      case 'ellipse':
        element = { ...base, type: 'ellipse', text: (options as any).text } as EllipseElement
        break
      case 'diamond':
        element = { ...base, type: 'diamond', text: (options as any).text } as DiamondElement
        break
      case 'text':
        element = { ...base, type: 'text', text: (options as any).text || 'Text' } as TextElement
        break
      case 'sticky-note':
        element = {
          ...base,
          type: 'sticky-note',
          text: (options as any).text,
          style: { ...base.style, fill: '#fef08a', stroke: '#eab308' },
        } as StickyNoteElement
        break
      case 'arrow': {
        const arrowOpts = options as Partial<ArrowElement>
        element = {
          ...base,
          type: 'arrow',
          connectionStyle: { ...DEFAULT_CONN_STYLE_VAL, ...(arrowOpts as any).connectionStyle },
          sourceId: arrowOpts.sourceId ?? null,
          targetId: arrowOpts.targetId ?? null,
          sourceAnchor: arrowOpts.sourceAnchor ?? 'right',
          targetAnchor: arrowOpts.targetAnchor ?? 'left',
          startPoint: arrowOpts.startPoint ?? { x: base.x, y: base.y },
          endPoint: arrowOpts.endPoint ?? { x: base.x + base.width, y: base.y },
          waypoints: arrowOpts.waypoints,
          controlPoints: arrowOpts.controlPoints,
          label: arrowOpts.label,
        } as ArrowElement
        break
      }
      case 'line': {
        const lineOpts = options as Partial<LineElement>
        element = {
          ...base,
          type: 'line',
          startPoint: lineOpts.startPoint ?? { x: base.x, y: base.y },
          endPoint: lineOpts.endPoint ?? { x: base.x + base.width, y: base.y },
          connectionStyle: { ...DEFAULT_CONN_STYLE_VAL, ...(lineOpts as any).connectionStyle },
          controlPoints: lineOpts.controlPoints,
        } as LineElement
        break
      }
      case 'freehand': {
        const freehandOpts = options as Partial<FreehandElement>
        element = {
          ...base,
          type: 'freehand',
          points: freehandOpts.points ?? [],
          smoothing: freehandOpts.smoothing ?? 0.5,
        } as FreehandElement
        break
      }
      case 'image': {
        const imgOpts = options as Partial<ImageElement>
        element = {
          ...base,
          type: 'image',
          src: imgOpts.src ?? '',
        } as ImageElement
        break
      }
      case 'group': {
        const groupOpts = options as Partial<GroupElement>
        element = {
          ...base,
          type: 'group',
          childIds: groupOpts.childIds ?? [],
        } as GroupElement
        break
      }
      default:
        element = { ...base, type: 'rectangle', text: (options as any).text } as RectangleElement
    }

    this._elements.set(id, element)

    // 添加到页面
    const page = this._pages.get(element.pageId)
    if (page && !page.elementIds.includes(id)) {
      page.elementIds.push(id)
    }

    this._notifyChange()
    return element
  }

  /** 更新元素 */
  updateElement(id: string, updates: Partial<BoardElement>): BoardElement | undefined {
    const el = this._elements.get(id)
    if (!el) return undefined

    const updated = { ...el, ...updates } as BoardElement
    this._elements.set(id, updated)
    this._notifyChange()
    return updated
  }

  /** 更新元素位置 */
  updateElementPosition(id: string, x: number, y: number): void {
    const el = this._elements.get(id)
    if (!el) return

    const dx = x - el.x
    const dy = y - el.y

    ;(el as any).x = x
    ;(el as any).y = y

    // 同步箭头/线条的端点坐标
    if ((el.type === 'arrow' || el.type === 'line') && ('startPoint' in el)) {
      ;(el as any).startPoint = {
        x: (el as any).startPoint.x + dx,
        y: (el as any).startPoint.y + dy,
      }
      ;(el as any).endPoint = {
        x: (el as any).endPoint.x + dx,
        y: (el as any).endPoint.y + dy,
      }
      // 同步贝塞尔控制点（如果有自定义控制点，随端点一起移动）
      const cp = (el as any).controlPoints
      if (cp) {
        ;(el as any).controlPoints = {
          cp1: { x: cp.cp1.x + dx, y: cp.cp1.y + dy },
          cp2: { x: cp.cp2.x + dx, y: cp.cp2.y + dy },
        }
      }
    }

    this._notifyChange()
  }

  /** 更新元素尺寸 */
  updateElementSize(id: string, width: number, height: number): void {
    const el = this._elements.get(id)
    if (el) {
      (el as any).width = width
      ;(el as any).height = height
      this._notifyChange()
    }
  }

  /** 更新元素旋转角度 */
  updateElementRotation(id: string, rotation: number): void {
    const el = this._elements.get(id)
    if (el) {
      (el as any).rotation = rotation
      this._notifyChange()
    }
  }

  /** 更新元素样式 */
  updateElementStyle(id: string, style: Partial<ElementStyle>): void {
    const el = this._elements.get(id)
    if (el) {
      el.style = { ...el.style, ...style }
      this._notifyChange()
    }
  }

  /** 删除元素 */
  removeElement(id: string): void {
    const el = this._elements.get(id)
    if (!el) return

    // 从页面移除
    const page = this._pages.get(el.pageId)
    if (page) {
      page.elementIds = page.elementIds.filter(eid => eid !== id)
    }

    // 删除相关连接
    this._connections.forEach((conn, connId) => {
      if (conn.sourceId === id || conn.targetId === id) {
        this._connections.delete(connId)
      }
    })

    // 如果是分组，解组
    if (el.type === 'group') {
      (el as GroupElement).childIds.forEach(cid => {
        const child = this._elements.get(cid)
        if (child) child.groupId = null
      })
    }

    this._elements.delete(id)
    this._notifyChange()
  }

  /** 删除多个元素 */
  removeElements(ids: string[]): void {
    ids.forEach(id => this.removeElement(id))
  }

  /** 移动元素到最前 */
  bringToFront(id: string): void {
    const el = this._elements.get(id)
    if (!el) return

    const pageElements = this.elements.filter(e => e.pageId === this._activePageId)
    const maxZ = Math.max(...pageElements.map(e => e.zIndex))
    if (el.zIndex === maxZ) return

    const topEl = pageElements.find(e => e.zIndex === maxZ)
    if (topEl && topEl.id !== id) {
      const tmp = topEl.zIndex
      topEl.zIndex = el.zIndex
      el.zIndex = tmp
    } else {
      el.zIndex = this._zIndexCounter++
    }
    this._notifyChange()
  }

  /** 移动元素到最后 */
  sendToBack(id: string): void {
    const el = this._elements.get(id)
    if (!el) return

    const pageElements = this.elements.filter(e => e.pageId === this._activePageId)
    const minZ = Math.min(...pageElements.map(e => e.zIndex))
    if (el.zIndex === minZ) return

    const bottomEl = pageElements.find(e => e.zIndex === minZ)
    if (bottomEl && bottomEl.id !== id) {
      const tmp = bottomEl.zIndex
      bottomEl.zIndex = el.zIndex
      el.zIndex = tmp
    } else {
      el.zIndex = minZ - 1
    }
    this._notifyChange()
  }

  // ===== 连接操作 =====

  /** 获取所有连接 */
  get connections(): Connection[] {
    return Array.from(this._connections.values())
  }

  /** 添加连接 */
  addConnection(sourceId: string, targetId: string, options: Partial<Connection> = {}): Connection | undefined {
    if (!this._elements.has(sourceId) || !this._elements.has(targetId)) return undefined

    const id = options.id || generateId('conn')
    const conn: Connection = {
      id,
      sourceId,
      targetId,
      sourceAnchor: options.sourceAnchor ?? 'right',
      targetAnchor: options.targetAnchor ?? 'left',
      style: { ...DEFAULT_CONN_STYLE_VAL, ...options.style },
      label: options.label,
      waypoints: options.waypoints,
    }

    this._connections.set(id, conn)
    this._notifyChange()
    return conn
  }

  /** 删除连接 */
  removeConnection(id: string): void {
    this._connections.delete(id)
    this._notifyChange()
  }

  /** 获取元素的连接 */
  getElementConnections(elementId: string): Connection[] {
    return this.connections.filter(c => c.sourceId === elementId || c.targetId === elementId)
  }

  /** 获取元素锚点世界坐标 */
  getElementAnchor(elementId: string, anchor: AnchorPosition): Point {
    const el = this._elements.get(elementId)
    if (!el) return { x: 0, y: 0 }

    const cx = el.x + el.width / 2
    const cy = el.y + el.height / 2
    const cos = Math.cos(el.rotation)
    const sin = Math.sin(el.rotation)

    let localX = 0, localY = 0
    switch (anchor) {
      case 'top':    localX = 0; localY = -el.height / 2; break
      case 'right':  localX = el.width / 2; localY = 0; break
      case 'bottom': localX = 0; localY = el.height / 2; break
      case 'left':   localX = -el.width / 2; localY = 0; break
      case 'center': localX = 0; localY = 0; break
    }

    return {
      x: cx + localX * cos - localY * sin,
      y: cy + localX * sin + localY * cos,
    }
  }

  // ===== 页面操作 =====

  /** 获取所有页面 */
  get pages(): BoardPage[] {
    return Array.from(this._pages.values())
  }

  /** 当前活动页面ID */
  get activePageId(): string {
    return this._activePageId
  }

  /** 当前活动页面 */
  get activePage(): BoardPage | undefined {
    return this._pages.get(this._activePageId)
  }

  /** 添加页面 */
  addPage(name?: string): BoardPage {
    const id = generatePageId()
    const page: BoardPage = {
      id,
      name: name || `Page ${this._pages.size + 1}`,
      elementIds: [],
    }
    this._pages.set(id, page)
    this._activePageId = id
    this._notifyChange()
    return page
  }

  /** 切换活动页面 */
  setActivePage(pageId: string): void {
    if (this._pages.has(pageId)) {
      this._activePageId = pageId
      this._notifyChange()
    }
  }

  /** 删除页面 */
  removePage(pageId: string): void {
    if (this._pages.size <= 1) return // 至少保留一页

    const page = this._pages.get(pageId)
    if (page) {
      // 删除页面上的所有元素
      page.elementIds.forEach(eid => this._elements.delete(eid))
      this._pages.delete(pageId)

      // 如果删除的是当前页面，切换到第一个页面
      if (this._activePageId === pageId) {
        this._activePageId = this._pages.keys().next().value!
      }
    }
    this._notifyChange()
  }

  /** 重命名页面 */
  renamePage(pageId: string, name: string): void {
    const page = this._pages.get(pageId)
    if (page) {
      page.name = name
      this._notifyChange()
    }
  }

  // ===== 序列化 =====

  /** 序列化为JSON */
  serialize(): SerializedBoard {
    return {
      elements: this.elements.map(el => {
        const base: Record<string, unknown> = {
          id: el.id,
          type: el.type,
          x: el.x,
          y: el.y,
          width: el.width,
          height: el.height,
        }
        if (el.rotation) base.rotation = el.rotation
        if (Object.keys(el.style).length > 0) base.style = el.style
        if (el.zIndex !== undefined) base.zIndex = el.zIndex
        if (el.locked) base.locked = true
        if (el.groupId) base.groupId = el.groupId
        base.pageId = el.pageId

        // 类型特有字段
        if ('text' in el && el.text) base.text = el.text
        if ('src' in el) base.src = (el as ImageElement).src
        if ('points' in el) base.points = (el as FreehandElement).points
        if ('childIds' in el) base.childIds = (el as GroupElement).childIds
        if ('connectionStyle' in el) base.connectionStyle = (el as ArrowElement).connectionStyle
        if ('sourceId' in el) {
          const arrow = el as ArrowElement
          base.sourceId = arrow.sourceId
          base.targetId = arrow.targetId
          base.sourceAnchor = arrow.sourceAnchor
          base.targetAnchor = arrow.targetAnchor
          base.startPoint = arrow.startPoint
          base.endPoint = arrow.endPoint
          if (arrow.waypoints) base.waypoints = arrow.waypoints
          if (arrow.controlPoints) base.controlPoints = arrow.controlPoints
          if (arrow.label) base.label = arrow.label
        }
        if ('startPoint' in el && el.type === 'line') {
          const line = el as LineElement
          base.startPoint = line.startPoint
          base.endPoint = line.endPoint
          base.connectionStyle = line.connectionStyle
          if (line.controlPoints) base.controlPoints = line.controlPoints
        }

        return base as any
      }),
      connections: this.connections.map(c => {
        const data: Record<string, unknown> = {
          id: c.id,
          sourceId: c.sourceId,
          targetId: c.targetId,
          sourceAnchor: c.sourceAnchor,
          targetAnchor: c.targetAnchor,
          style: c.style,
        }
        if (c.label) data.label = c.label
        if (c.waypoints) data.waypoints = c.waypoints
        return data as any
      }),
      pages: this.pages.map(p => ({ ...p })),
      activePageId: this._activePageId,
    }
  }

  /** 从JSON加载 */
  static deserialize(data: SerializedBoard): BoardModel {
    const model = new BoardModel()
    model.loadElements(data.elements)
    model.loadConnections(data.connections)

    if (data.pages && data.pages.length > 0) {
      model._pages.clear()
      data.pages.forEach(p => model._pages.set(p.id, { ...p }))
      // Restore elementIds from loaded elements (deserialize may have created fresh pages via constructor)
      model._pages.forEach(page => {
        page.elementIds = data.elements
          .filter(el => el.pageId === page.id)
          .map(el => el.id)
      })
    }
    if (data.activePageId) {
      model._activePageId = data.activePageId
    }

    return model
  }

  // ===== 私有方法 =====

  private loadElements(elements: BoardElement[]): void {
    elements.forEach(el => {
      this._elements.set(el.id, el)
      if (el.zIndex >= this._zIndexCounter) {
        this._zIndexCounter = el.zIndex + 1
      }
    })
  }

  private loadConnections(connections: Connection[]): void {
    connections.forEach(c => {
      this._connections.set(c.id, c)
    })
  }

  private _notifyChange(): void {
    this._changeListeners.forEach(listener => listener())
  }

  // ===== 变更监听 =====

  onChange(listener: () => void): () => void {
    this._changeListeners.add(listener)
    return () => this._changeListeners.delete(listener)
  }

  // ===== 工具方法 =====

  /** 创建默认白板 */
  static createDefault(): BoardModel {
    return new BoardModel()
  }

  /** 克隆 */
  clone(): BoardModel {
    return BoardModel.deserialize(this.serialize())
  }

  /** 获取元素总数 */
  get elementCount(): number {
    return this._elements.size
  }

  /** 获取连接总数 */
  get connectionCount(): number {
    return this._connections.size
  }

  /** 计算元素的包围盒 */
  getElementBounds(ids: string[]): { x: number; y: number; width: number; height: number } | null {
    if (ids.length === 0) return null
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
    for (const id of ids) {
      const el = this._elements.get(id)
      if (!el) continue
      minX = Math.min(minX, el.x)
      minY = Math.min(minY, el.y)
      maxX = Math.max(maxX, el.x + el.width)
      maxY = Math.max(maxY, el.y + el.height)
    }
    if (minX === Infinity) return null
    return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
  }
}
