# -*- coding: utf-8 -*-
"""
JeecgBoot 大屏/仪表盘设计器 Python 工具库
用于通过 API 自动创建和管理大屏、仪表盘页面及其组件。

使用方式：
    from bi_utils import *
    init_api('https://api3.boot.jeecg.com', 'your-token')
    page_id = create_page('销售大屏', style='bigScreen')
    add_number(page_id, '销售额', x=50, y=50, w=400, h=200, value=128560)
    add_chart(page_id, 'JBar', '月度销售', x=50, y=280, w=860, h=380,
              categories=['1月','2月','3月'], series=[{'name':'销售额','data':[820,932,901]}])
    save_page(page_id)
"""

import json
import urllib.request
import urllib.parse
import time
import random
import uuid

# ============================================================
# 全局配置
# ============================================================
API_BASE = ''
TOKEN = ''

# 内存中缓存页面组件数据，save_page 时一次性提交
_page_components = {}  # {page_id: [component_dict, ...]}
_page_info = {}        # {page_id: {name, style, theme, ...}}


# ============================================================
# 大屏 vs 仪表盘 模式预设（颜色、样式完全不同）
# ============================================================
# 大屏（bigScreen）- 深色背景，亮色文字
_BIGSCREEN = {
    'bg': 'rgba(0,0,0,0)',
    'border_color': '',
    'title_color': '#ffffff',
    'axis_color': '#ffffff',
    'grid_color': 'rgba(255,255,255,0.1)',
    'body_color': '#ffffff',
    'suffix_color': '#ffffff',
    'legend_color': '#ffffff',
    'tooltip_color': '#ffffff',
    'card': {'title': '', 'extra': '', 'rightHref': '', 'size': 'small'},
    'number_font_size': 32,
    # 表格
    'table_header_bg': 'rgba(0,0,0,0.3)',
    'table_header_color': '#ffffff',
    'table_body_bg': 'rgba(0,0,0,0.1)',
    'table_body_color': '#ffffff',
    'table_body_font_size': 14,
    'table_header_font_size': 14,
    # 滚动表格
    'scroll_odd_color': '#0a2732',
    'scroll_even_color': '#003b51',
    'scroll_header_bg': '#0a73ff',
    'scroll_header_color': '#ffffff',
    'scroll_body_color': '#ffffff',
    'scroll_border_color': 'rgba(255,255,255,0.1)',
    # 排行榜
    'ranking_color': '#1370fb',
    'ranking_text_color': '#fff',
}

# 仪表盘（default）- 亮色背景，深色文字，带卡片头
_DASHBOARD = {
    'bg': '#FFFFFF',
    'border_color': '#E8E8E8',
    'title_color': '#464646',
    'axis_color': '#909198',
    'grid_color': '#F3F3F3',
    'body_color': '#464646',
    'suffix_color': '#909198',
    'legend_color': '#464646',
    'tooltip_color': '#464646',
    'card': {
        'title': '',  # 由各函数填充
        'extra': '', 'rightHref': '',
        'size': 'default',
        'headColor': '#FFFFFF',
        'textStyle': {'color': '#464646', 'fontSize': 16, 'fontWeight': 'bold'},
    },
    'number_font_size': 32,
    # 表格
    'table_header_bg': '#FAFAFA',
    'table_header_color': '#464646',
    'table_body_bg': '#FFFFFF',
    'table_body_color': '#666666',
    'table_body_font_size': 13,
    'table_header_font_size': 14,
    # 滚动表格
    'scroll_odd_color': '#FFFFFF',
    'scroll_even_color': '#FAFAFA',
    'scroll_header_bg': '#F0F0F0',
    'scroll_header_color': '#464646',
    'scroll_body_color': '#666666',
    'scroll_border_color': '#E8E8E8',
    # 排行榜
    'ranking_color': '#1890FF',
    'ranking_text_color': '#464646',
}


def _get_mode(page_id):
    """获取页面模式预设（大屏 or 仪表盘）"""
    info = _page_info.get(page_id, {})
    style = info.get('style', 'bigScreen')
    if style == 'default':
        return _DASHBOARD
    return _BIGSCREEN


def _make_card(mode, title):
    """根据模式创建 card 配置。
    大屏模式：card.title 保持为空（标题由 ECharts option.title 显示，避免重复）
    仪表盘模式：card.title 设置标题（卡片头显示）
    """
    card = dict(mode['card'])
    if 'textStyle' in mode['card']:
        card['textStyle'] = dict(mode['card']['textStyle'])
    # 大屏不用 card 标题头，仪表盘用
    if mode is _DASHBOARD:
        card['title'] = title
    else:
        card['title'] = ''
    return card


def init_api(api_base, token):
    """初始化 API 地址和 Token"""
    global API_BASE, TOKEN
    API_BASE = api_base.rstrip('/')
    TOKEN = token
    print(f'[bi_utils] API: {API_BASE}')


# ============================================================
# HTTP 工具
# ============================================================
def _request(method, path, data=None, params=None):
    """发送 HTTP 请求"""
    url = f'{API_BASE}{path}'
    if params:
        url += '?' + urllib.parse.urlencode(params)

    headers = {
        'Content-Type': 'application/json;charset=UTF-8',
        'X-Access-Token': TOKEN,
    }

    body = None
    if data is not None:
        body = json.dumps(data, ensure_ascii=False).encode('utf-8')

    req = urllib.request.Request(url, data=body, headers=headers, method=method)
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            result = json.loads(resp.read().decode('utf-8'))
            return result
    except urllib.error.HTTPError as e:
        error_body = e.read().decode('utf-8') if e.fp else ''
        print(f'[bi_utils] HTTP {e.code}: {error_body}')
        raise
    except Exception as e:
        print(f'[bi_utils] Request error: {e}')
        raise


def _gen_key():
    """生成唯一 key"""
    return f'{int(time.time() * 1000)}_{random.randint(100000, 999999)}'


def _gen_uuid():
    """生成 32 位无横线 UUID"""
    return uuid.uuid4().hex


# ============================================================
# 页面管理 API
# ============================================================
def create_page(name, style='bigScreen', theme=None, background_image=None,
                type_id='0', design_type=100, protection_code=''):
    """
    创建大屏或仪表盘页面。

    Args:
        name: 页面名称
        style: 'bigScreen'=大屏, 'default'=仪表盘
        theme: 主题，默认大屏=dark，仪表盘=default
        background_image: 背景图路径，大屏默认 '/img/bg/bg4.png'
        type_id: 分类 ID，默认 '0'
        design_type: 设计类型 100=PC, 30=手机, 80=平板
        protection_code: 保护密码
    Returns:
        page_id: 页面 ID
    """
    if theme is None:
        theme = 'dark' if style == 'bigScreen' else 'default'
    if background_image is None and style == 'bigScreen':
        background_image = '/img/bg/bg4.png'

    payload = {
        'name': name,
        'type': type_id,
        'protectionCode': protection_code,
        'theme': theme,
        'style': style,
        'backgroundImage': background_image or '',
        'designType': design_type,
    }

    result = _request('POST', '/drag/page/add', data=payload)

    if not result.get('success'):
        raise Exception(f"创建页面失败: {result.get('message', json.dumps(result, ensure_ascii=False))}")

    page_data = result.get('result', {})
    page_id = page_data.get('id')
    if not page_id:
        raise Exception(f"创建页面成功但未返回 ID: {json.dumps(result, ensure_ascii=False)}")

    # 缓存页面信息
    _page_components[page_id] = []
    _page_info[page_id] = {
        'name': name,
        'style': style,
        'theme': theme,
        'backgroundImage': background_image or '',
        'designType': design_type,
        'updateCount': page_data.get('updateCount', 1),
        'path': page_data.get('path', ''),
    }

    print(f'[bi_utils] 页面创建成功: {name} (ID: {page_id})')
    return page_id


def query_page(page_id):
    """查询页面详情（含组件配置）"""
    result = _request('GET', '/drag/page/queryById', params={'id': page_id})
    if not result.get('success'):
        raise Exception(f"查询页面失败: {result.get('message')}")
    page = result.get('result', {})

    # 更新缓存
    if page_id not in _page_info:
        _page_info[page_id] = {}
    _page_info[page_id]['updateCount'] = page.get('updateCount', 1)
    _page_info[page_id]['name'] = page.get('name', '')

    # 解析 template
    template = page.get('template')
    if template and isinstance(template, str):
        try:
            page['template'] = json.loads(template)
        except:
            pass

    return page


def list_pages(style=None, page_no=1, page_size=50):
    """列表查询页面"""
    params = {'pageNo': page_no, 'pageSize': page_size}
    if style:
        params['style'] = style
    result = _request('GET', '/drag/page/list', params=params)
    if not result.get('success'):
        raise Exception(f"查询列表失败: {result.get('message')}")
    return result.get('result', {})


def save_page(page_id):
    """
    保存页面设计（将所有缓存的组件一次性提交）。

    通过 POST /drag/page/edit 提交，后端会：
    1. 删除所有旧的 OnlDragPageComp 记录
    2. 从 template 中提取 config 创建新的 comp 记录
    3. 更新 template（移除 config，注入 pageCompId）
    """
    components = _page_components.get(page_id, [])
    info = _page_info.get(page_id, {})

    # 始终查询最新页面信息，确保 updateCount 正确
    try:
        page = query_page(page_id)
        info = _page_info.get(page_id, {})
        # 如果本地没有新增组件，使用已有的
        if not components:
            existing_template = page.get('template', [])
            if isinstance(existing_template, list):
                components = existing_template
    except Exception as e:
        print(f'[bi_utils] 查询页面警告: {e}，使用缓存信息')

    # 构建 template JSON
    template = json.dumps(components, ensure_ascii=False)

    payload = {
        'id': page_id,
        'name': info.get('name', ''),
        'template': template,
        'updateCount': info.get('updateCount', 1),
        'style': info.get('style', 'bigScreen'),
        'theme': info.get('theme', 'dark'),
        'backgroundImage': info.get('backgroundImage', ''),
        'designType': info.get('designType', 100),
    }

    result = _request('POST', '/drag/page/edit', data=payload)

    if not result.get('success'):
        raise Exception(f"保存页面失败: {result.get('message')}")

    # 更新 updateCount
    new_count = result.get('result', {})
    if isinstance(new_count, dict):
        info['updateCount'] = new_count.get('updateCount', info.get('updateCount', 1) + 1)
    else:
        info['updateCount'] = info.get('updateCount', 1) + 1

    print(f'[bi_utils] 页面保存成功: {info.get("name", page_id)} ({len(components)} 个组件)')
    return True


def delete_page(page_id, physical=False):
    """
    删除页面。

    Args:
        page_id: 页面 ID
        physical: True=物理删除（彻底），False=逻辑删除（回收站）
    """
    if physical:
        result = _request('DELETE', '/drag/page/physicalDelete', params={'id': page_id})
    else:
        result = _request('DELETE', '/drag/page/delete', params={'id': page_id})

    if not result.get('success'):
        raise Exception(f"删除页面失败: {result.get('message')}")

    # 清理缓存
    _page_components.pop(page_id, None)
    _page_info.pop(page_id, None)

    print(f'[bi_utils] 页面删除成功: {page_id} ({"物理删除" if physical else "逻辑删除"})')
    return True


def recover_page(page_id):
    """恢复回收站中的页面"""
    result = _request('POST', '/drag/page/recoveryDelete', data={'id': page_id})
    if not result.get('success'):
        raise Exception(f"恢复页面失败: {result.get('message')}")
    print(f'[bi_utils] 页面恢复成功: {page_id}')
    return True


def copy_page(page_id):
    """复制页面"""
    result = _request('GET', '/drag/page/copyPage', params={'id': page_id})
    if not result.get('success'):
        raise Exception(f"复制页面失败: {result.get('message')}")
    new_page = result.get('result', {})
    new_id = new_page.get('id')
    print(f'[bi_utils] 页面复制成功: {page_id} → {new_id}')
    return new_id


# ============================================================
# 组件添加函数
# ============================================================
def add_component(page_id, component, title, x, y, w, h, config=None):
    """
    添加通用组件到页面。

    Args:
        page_id: 页面 ID
        component: 组件类型，如 'JBar', 'JNumber', 'JTable'
        title: 组件标题
        x, y: 位置（大屏=像素，仪表盘=栅格）
        w, h: 尺寸（大屏=像素，仪表盘=栅格）
        config: 组件配置 dict（可选，会与默认配置合并）
    Returns:
        component dict（已加入缓存）
    """
    if page_id not in _page_components:
        _page_components[page_id] = []

    key = _gen_key()

    # 栅格单位转换为像素（仪表盘模式）
    info = _page_info.get(page_id, {})
    style = info.get('style', 'bigScreen')
    if style == 'default':
        px_w = w * 75
        px_h = h * 11
    else:
        px_w = w
        px_h = h

    # 基础配置
    default_config = {
        'dataType': 1,
        'timeOut': 0,
        'size': {'width': px_w, 'height': px_h},
        'chart': {
            'subclass': component,
            'category': _get_category(component),
        },
        'option': {},
        'chartData': [],
        'linkageConfig': [],
        'turnConfig': {'url': '', 'type': '_blank'},
        'linkType': 'url',
    }

    # 合并用户配置
    if config:
        _deep_merge(default_config, config)

    comp = {
        'component': component,
        'i': key,
        'x': x,
        'y': y,
        'w': w,
        'h': h,
        'pcX': x,
        'pcY': y,
        'pcW': w,
        'orderNum': len(_page_components[page_id]),
        'config': json.dumps(default_config, ensure_ascii=False),
    }

    _page_components[page_id].append(comp)
    return comp


def add_number(page_id, title, x, y, w, h, value=0, prefix='', suffix='',
               font_size=None, color=None, bg_color=None):
    """
    添加数字指标组件（JNumber）。
    自动根据页面模式（大屏/仪表盘）应用不同默认样式。

    Args:
        value: 显示的数值
        prefix: 前缀（如 '¥'）
        suffix: 后缀（如 '元', '%'）
        font_size: 字体大小（默认根据模式自动设置）
        color: 字体颜色（默认根据模式自动设置）
    """
    mode = _get_mode(page_id)
    if font_size is None:
        font_size = mode['number_font_size']
    if color is None:
        color = mode['body_color']

    config = {
        'dataType': 1,
        'chartData': json.dumps({'value': value}, ensure_ascii=False),
        'background': mode['bg'],
        'borderColor': mode['border_color'],
        'option': {
            'title': {'text': title, 'show': True,
                      'textStyle': {'color': mode['title_color'], 'fontSize': 14}},
            'body': {
                'text': '',
                'color': color,
                'fontSize': font_size,
                'fontWeight': 'bold',
                'marginLeft': 0,
                'marginTop': 0,
            },
            'card': _make_card(mode, title),
            'prefix': prefix,
            'prefixColor': mode['suffix_color'],
            'prefixFontSize': 14,
            'suffix': suffix,
            'suffixColor': mode['suffix_color'],
            'suffixFontSize': 14,
            'isCompare': False,
            'trendType': '1',
        },
        'analysis': {
            'isCompare': False,
            'compareType': '',
            'trendType': '1',
        },
    }
    return add_component(page_id, 'JNumber', title, x, y, w, h, config)


def add_chart(page_id, chart_type, title, x, y, w, h,
              categories=None, series=None, pie_data=None):
    """
    添加图表组件。

    支持的 chart_type: JBar, JLine, JSmoothLine, JHorizontalBar, JStackBar,
    JMixLineBar, DoubleLineBar, JPie, JRing, JRose, JFunnel, JRadar, JScatter,
    JGauge, JLiquid, JProgress, JWordCloud, JAreaMap, JFlyLineMap, 等等。

    Args:
        chart_type: 组件类型
        categories: X轴类目数据 ['一月','二月',...]（轴类图表用）
        series: 系列数据 [{'name':'系列1','data':[1,2,3]}]（轴类图表用）
        pie_data: 饼图数据 [{'name':'A','value':10}]（饼/环/玫瑰图用）
    """
    # 确定图表 ECharts 类型
    echart_type_map = {
        'JBar': 'bar', 'JHorizontalBar': 'bar', 'JBackgroundBar': 'bar',
        'JMultipleBar': 'bar', 'JNegativeBar': 'bar', 'JStackBar': 'bar',
        'JLine': 'line', 'JSmoothLine': 'line', 'JStepLine': 'line',
        'JMultipleLine': 'line',
        'JMixLineBar': 'bar',  # 混合类型
        'JPie': 'pie', 'JRing': 'pie', 'JRose': 'pie',
        'JFunnel': 'funnel',
        'JRadar': 'radar',
        'JScatter': 'scatter', 'JBubble': 'scatter',
        'JGauge': 'gauge',
    }
    echart_type = echart_type_map.get(chart_type, 'bar')

    # 构建 chartData
    chart_data = []
    if pie_data:
        chart_data = pie_data
    elif categories and series:
        if len(series) == 1:
            # 单系列：简单 name/value
            for i, cat in enumerate(categories):
                chart_data.append({
                    'name': cat,
                    'value': series[0]['data'][i] if i < len(series[0]['data']) else 0,
                })
        else:
            # 多系列：需要 type 字段区分
            for s in series:
                for i, cat in enumerate(categories):
                    chart_data.append({
                        'name': cat,
                        'value': s['data'][i] if i < len(s['data']) else 0,
                        'type': s.get('name', ''),
                    })

    mode = _get_mode(page_id)

    # 构建 ECharts option（根据模式应用不同颜色）
    option = {
        'title': {'text': title, 'show': True,
                  'textStyle': {'color': mode['title_color'], 'fontSize': 16},
                  'subtextStyle': {'color': mode['axis_color']}},
        'tooltip': {'show': True, 'textStyle': {'color': mode['tooltip_color']}},
        'legend': {'show': len(series or []) > 1 or bool(pie_data),
                   'textStyle': {'color': mode['legend_color'], 'fontSize': 12}},
        'grid': {'left': 60, 'right': 30, 'top': 70, 'bottom': 40, 'show': False},
        'card': _make_card(mode, title),
    }

    if pie_data:
        # 饼图系列
        radius = '55%'
        if chart_type == 'JRing':
            radius = ['40%', '55%']
        elif chart_type == 'JRose':
            radius = ['20%', '55%']

        option['tooltip']['trigger'] = 'item'
        if chart_type in ('JPie', 'JRing', 'JRose'):
            option['legend']['orient'] = 'vertical'
        option['series'] = [{
            'name': title,
            'type': 'pie',
            'radius': radius,
            'data': pie_data,
            'emphasis': {'itemStyle': {'shadowBlur': 10, 'shadowOffsetX': 0,
                                       'shadowColor': 'rgba(0,0,0,0.5)'}},
        }]
    elif categories and series:
        # 轴类图表
        option['xAxis'] = {
            'type': 'category',
            'show': True,
            'data': categories,
            'axisLabel': {'color': mode['axis_color']},
            'axisLine': {'lineStyle': {'color': mode['grid_color']}},
        }
        option['yAxis'] = {
            'type': 'value',
            'show': True,
            'axisLabel': {'color': mode['axis_color']},
            'splitLine': {'lineStyle': {'color': mode['grid_color']}},
        }

        if chart_type == 'JHorizontalBar':
            option['xAxis'], option['yAxis'] = option['yAxis'], option['xAxis']
            option['yAxis']['data'] = categories
            option['yAxis']['type'] = 'category'
            option['xAxis'] = {
                'type': 'value', 'show': True,
                'axisLabel': {'color': mode['axis_color']},
                'axisLine': {'lineStyle': {'color': mode['grid_color']}},
                'splitLine': {'lineStyle': {'color': mode['grid_color']}},
            }

        option['series'] = []
        for s in series:
            series_item = {
                'name': s.get('name', ''),
                'type': echart_type,
                'data': s.get('data', []),
            }
            if chart_type == 'JSmoothLine':
                series_item['smooth'] = True
            if chart_type == 'JStepLine':
                series_item['step'] = 'middle'
            if chart_type == 'JStackBar':
                series_item['stack'] = 'total'
            option['series'].append(series_item)

    # 仪表盘模式：card.title 留空，仅用 option.title 显示标题（避免重复）
    option['card'] = _make_card(mode, '')

    config = {
        'dataType': 1,
        'chartData': json.dumps(chart_data, ensure_ascii=False),
        'background': mode['bg'],
        'borderColor': mode['border_color'],
        'option': option,
    }

    return add_component(page_id, chart_type, title, x, y, w, h, config)


def add_table(page_id, title, x, y, w, h, columns=None, data=None):
    """
    添加数据表格组件（JTable）。

    Args:
        columns: 列名列表 ['姓名', '年龄', '地址']
        data: 数据行列表 [{'姓名':'张三','年龄':'28','地址':'北京'}]
    """
    columns = columns or []
    data = data or []

    chart_data = []
    for col in columns:
        field_name = col.lower().replace(' ', '_')
        chart_data.append({
            'fieldTxt': col,
            'fieldName': field_name,
            'type': 'field',
            'isShow': 'Y',
            'isTotal': 'N',
        })

    mode = _get_mode(page_id)

    config = {
        'dataType': 1,
        'chartData': json.dumps(chart_data, ensure_ascii=False),
        'background': mode['bg'],
        'borderColor': mode['border_color'],
        'option': {
            'title': {'text': title, 'show': True,
                      'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
            'bordered': True,
            'size': 'small',
            'headerBgColor': mode['table_header_bg'],
            'headerColor': mode['table_header_color'],
            'headerFontSize': mode['table_header_font_size'],
            'bodyBgColor': mode['table_body_bg'],
            'bodyColor': mode['table_body_color'],
            'bodyFontSize': mode['table_body_font_size'],
            'card': _make_card(mode, title),
            'data': data,
        },
    }

    return add_component(page_id, 'JTable', title, x, y, w, h, config)


def add_scroll_table(page_id, title, x, y, w, h, columns=None, data=None):
    """
    添加自动滚动表格组件（JScrollTable）。

    Args:
        columns: 列名列表
        data: 数据行列表（二维数组格式）
    """
    columns = columns or []
    data = data or []

    # JScrollTable expects chartData as array of objects
    # columns maps to option.fieldMapping
    field_mapping = []
    for col in columns:
        field_name = col.lower().replace(' ', '_')
        field_mapping.append({'name': col, 'key': field_name, 'width': 0})

    # Convert data rows: if data is list of lists, convert to list of dicts
    chart_data = []
    if data and isinstance(data[0], (list, tuple)):
        for row in data:
            item = {}
            for j, col in enumerate(columns):
                field_name = col.lower().replace(' ', '_')
                item[field_name] = row[j] if j < len(row) else ''
            chart_data.append(item)
    elif data and isinstance(data[0], dict):
        chart_data = data
    else:
        chart_data = data or []

    mode = _get_mode(page_id)

    config = {
        'dataType': 1,
        'chartData': json.dumps(chart_data, ensure_ascii=False),
        'background': mode['bg'],
        'borderColor': mode['border_color'],
        'option': {
            'title': {'text': title, 'show': True,
                      'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
            'ranking': False,
            'textPosition': 'center',
            'lineHeight': 50,
            'fontSize': 20,
            'bodyFontSize': 18,
            'scrollTime': 50,
            'scroll': True,
            'showBorder': True,
            'borderWidth': 1,
            'borderColor': mode['scroll_border_color'],
            'borderStyle': 'solid',
            'showHead': True,
            'bodyFontColor': mode['scroll_body_color'],
            'oddColor': mode['scroll_odd_color'],
            'evenColor': mode['scroll_even_color'],
            'headerBgColor': mode['scroll_header_bg'],
            'headerFontColor': mode['scroll_header_color'],
            'fieldMapping': field_mapping,
            'card': _make_card(mode, title),
        },
    }

    return add_component(page_id, 'JScrollTable', title, x, y, w, h, config)


def add_ranking(page_id, title, x, y, w, h, data=None):
    """
    添加排行榜组件（JScrollRankingBoard）。

    Args:
        data: 排行数据 [{'name':'项目A','value':100}, ...]
    """
    data = data or []

    mode = _get_mode(page_id)

    config = {
        'dataType': 1,
        'chartData': json.dumps(data, ensure_ascii=False),
        'background': mode['bg'],
        'borderColor': mode['border_color'],
        'option': {
            'title': {'text': title, 'show': True,
                      'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
            'waitTime': 2000,
            'rowNum': 5,
            'carousel': 'single',
            'sort': True,
            'fontSize': 13,
            'color': mode['ranking_color'],
            'textColor': mode['ranking_text_color'],
            'card': _make_card(mode, title),
        },
    }

    return add_component(page_id, 'JScrollRankingBoard', title, x, y, w, h, config)


def add_text(page_id, title, x, y, w, h, content='', font_size=16, color=None,
             font_weight='normal', text_align='left', letter_spacing=0):
    """
    添加文本组件（JText）。
    使用与真实模板一致的 option.body 结构 + chartData: {"value": "..."} 格式。

    Args:
        content: 文本内容
        font_size: 字体大小
        color: 字体颜色（默认根据模式自动设置）
        font_weight: 字体粗细 'normal'/'bold'
        text_align: 对齐方式 'left'/'center'/'right'
        letter_spacing: 字间距（大屏标题建议 5-8）
    """
    mode = _get_mode(page_id)
    if color is None:
        color = mode['title_color']

    text_value = content or title
    config = {
        'dataType': 1,
        'chartData': {'value': text_value},
        'option': {
            'body': {
                'color': color,
                'fontSize': font_size,
                'fontWeight': font_weight,
                'letterSpacing': letter_spacing,
                'text': '',
                'marginTop': 0,
                'marginLeft': 0,
            },
            'textAlign': text_align,
            'card': {'title': '', 'extra': '', 'rightHref': '', 'size': 'default'},
            'openUrl': '',
            'isLink': False,
            'openType': '_blank',
        },
    }

    return add_component(page_id, 'JText', title, x, y, w, h, config)


def add_image(page_id, title, x, y, w, h, src=''):
    """
    添加图片组件（JImg）。

    Args:
        src: 图片 URL
    """
    config = {
        'dataType': 1,
        'chartData': src,
        'option': {
            'objectFit': 'cover',
        },
    }

    return add_component(page_id, 'JImg', title, x, y, w, h, config)


def add_gauge(page_id, title, x, y, w, h, value=0, max_val=100,
              unit='%', color='#00BAFF'):
    """
    添加仪表盘组件（JGauge）。

    Args:
        value: 当前值
        max_val: 最大值
        unit: 单位
        color: 指针颜色
    """
    mode = _get_mode(page_id)
    tail_color = '#333' if mode is _BIGSCREEN else '#E8E8E8'

    config = {
        'dataType': 1,
        'chartData': json.dumps([{'name': title, 'value': value}], ensure_ascii=False),
        'background': mode['bg'],
        'borderColor': mode['border_color'],
        'option': {
            'title': {'text': title, 'show': True,
                      'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
            'card': _make_card(mode, title),
            'series': [{
                'type': 'gauge',
                'max': max_val,
                'detail': {'formatter': f'{{value}}{unit}'},
                'data': [{'value': value, 'name': title}],
                'axisLine': {
                    'lineStyle': {
                        'color': [[value / max_val, color], [1, tail_color]],
                    }
                },
            }],
        },
    }

    return add_component(page_id, 'JGauge', title, x, y, w, h, config)


def add_liquid(page_id, title, x, y, w, h, value=50, color='#00BAFF'):
    """
    添加水球图组件（JLiquid）。

    Args:
        value: 0~100 的百分比值（如 97.3 表示 97.3%）
        color: 颜色
    """
    mode = _get_mode(page_id)

    config = {
        'dataType': 1,
        'chartData': json.dumps([{'value': value}], ensure_ascii=False),
        'background': mode['bg'],
        'borderColor': mode['border_color'],
        'option': {
            'title': {'text': title, 'show': True,
                      'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
            'liquidType': 'circle',
            'color': color,
            'borderWidth': 2,
            'distance': 1,
            'borderColor': color,
            'strokeOpacity': 0,
            'count': 4,
            'length': 128,
            'textColor': mode['title_color'],
            'textFontSize': 30,
            'card': _make_card(mode, title),
        },
    }

    return add_component(page_id, 'JLiquid', title, x, y, w, h, config)


def add_countdown(page_id, title, x, y, w, h, value=0, font_size=48, color='#00BAFF'):
    """
    添加数字翻牌器组件（JCountTo）。

    Args:
        value: 目标数值
        font_size: 字体大小
        color: 字体颜色
    """
    mode = _get_mode(page_id)
    if color == '#00BAFF' and mode is _DASHBOARD:
        color = mode['body_color']

    config = {
        'dataType': 1,
        'chartData': str(value),
        'background': mode['bg'],
        'borderColor': mode['border_color'],
        'option': {
            'title': {'text': title, 'show': True,
                      'textStyle': {'color': mode['title_color'], 'fontSize': 16}},
            'endVal': value,
            'fontSize': font_size,
            'color': color,
            'duration': 2000,
            'card': _make_card(mode, title),
        },
    }

    return add_component(page_id, 'JCountTo', title, x, y, w, h, config)


def add_border(page_id, x, y, w, h, border_type=1, color='#00BAFF'):
    """
    添加装饰边框组件（JDragBorder）。

    Args:
        border_type: 边框样式 1~13
        color: 边框颜色
    """
    config = {
        'dataType': 1,
        'option': {
            'borderType': border_type,
            'color': [color],
        },
    }

    return add_component(page_id, 'JDragBorder', f'边框{border_type}', x, y, w, h, config)


def add_decoration(page_id, x, y, w, h, deco_type=1, color='#00BAFF'):
    """
    添加装饰条组件（JDragDecoration）。

    Args:
        deco_type: 装饰样式 1~12
        color: 颜色
    """
    config = {
        'dataType': 1,
        'option': {
            'decorationType': deco_type,
            'color': [color],
        },
    }

    return add_component(page_id, 'JDragDecoration', f'装饰{deco_type}', x, y, w, h, config)


# ============================================================
# 编辑已有页面
# ============================================================
def update_page(page_id, new_components=None):
    """
    更新已有页面的组件。

    Args:
        page_id: 页面 ID
        new_components: 新的组件列表（完全替换）
    """
    # 查询当前页面信息
    page = query_page(page_id)

    if new_components is not None:
        _page_components[page_id] = new_components
    elif page_id not in _page_components:
        # 从已有页面加载组件
        template = page.get('template', [])
        if isinstance(template, str):
            try:
                template = json.loads(template)
            except:
                template = []
        _page_components[page_id] = template

    return save_page(page_id)


def add_to_existing(page_id, component_func, *args, **kwargs):
    """
    向已有页面追加组件。先加载已有组件，再添加新组件，最后保存。

    用法：
        add_to_existing(page_id, add_number, '新指标', x=500, y=0, w=400, h=200, value=999)
    """
    if page_id not in _page_components or not _page_components[page_id]:
        # 先加载已有组件
        page = query_page(page_id)
        template = page.get('template', [])
        if isinstance(template, str):
            try:
                template = json.loads(template)
            except:
                template = []
        _page_components[page_id] = template

    # 调用组件添加函数
    return component_func(page_id, *args, **kwargs)


# ============================================================
# 菜单 SQL 生成
# ============================================================
ROLE_ID = 'f6817f48af4fb3af11b9e8bf182f618b'


def gen_menu_sql(parent_name, children, icon='ant-design:appstore-outlined', role_id=None):
    """
    生成菜单 SQL + 角色授权 SQL。

    Args:
        parent_name: 父菜单名称
        children: [(名称, page_id, sort_no), ...]
        icon: 父菜单图标
        role_id: 角色 ID，默认使用 ROLE_ID

    Returns:
        SQL 字符串
    """
    rid = role_id or ROLE_ID
    parent_id = _gen_uuid()
    lines = []

    # 父菜单
    lines.append(f"-- 父菜单: {parent_name}")
    lines.append(
        f"INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, "
        f"redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, "
        f"is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, "
        f"create_by, create_time, update_by, update_time, internal_or_external) "
        f"VALUES ('{parent_id}', NULL, '{parent_name}', '/{parent_id}', "
        f"'layouts/RouteView', NULL, NULL, 0, NULL, '1', 1.00, 0, '{icon}', "
        f"1, 0, 0, 0, 0, NULL, '1', 0, 0, 'admin', now(), NULL, NULL, 0);"
    )
    rp_id = _gen_uuid()
    lines.append(
        f"INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, "
        f"operate_date, operate_ip) VALUES ('{rp_id}', '{rid}', '{parent_id}', NULL, "
        f"now(), '127.0.0.1');"
    )
    lines.append('')

    # 子菜单
    for name, page_id, sort_no in children:
        menu_id = _gen_uuid()
        lines.append(f"-- 子菜单: {name}")
        lines.append(
            f"INSERT INTO sys_permission(id, parent_id, name, url, component, component_name, "
            f"redirect, menu_type, perms, perms_type, sort_no, always_show, icon, is_route, "
            f"is_leaf, keep_alive, hidden, hide_tab, description, status, del_flag, rule_flag, "
            f"create_by, create_time, update_by, update_time, internal_or_external) "
            f"VALUES ('{menu_id}', '{parent_id}', '{name}', "
            f"'/drag/page/view/{page_id}', "
            f"'super/drag/page/dashboardPreview', 'dashboardPreview', "
            f"NULL, 0, NULL, '1', {sort_no}.00, 0, NULL, 0, 1, 0, 0, 0, NULL, '1', "
            f"0, 0, 'admin', now(), NULL, NULL, 0);"
        )
        rp_id2 = _gen_uuid()
        lines.append(
            f"INSERT INTO sys_role_permission (id, role_id, permission_id, data_rule_ids, "
            f"operate_date, operate_ip) VALUES ('{rp_id2}', '{rid}', '{menu_id}', NULL, "
            f"now(), '127.0.0.1');"
        )
        lines.append('')

    return '\n'.join(lines)


# ============================================================
# 辅助函数
# ============================================================
def _get_category(component):
    """根据组件类型获取分类"""
    category_map = {
        'JBar': 'Bar', 'JHorizontalBar': 'Bar', 'JBackgroundBar': 'Bar',
        'JMultipleBar': 'Bar', 'JNegativeBar': 'Bar', 'JStackBar': 'Bar',
        'JDynamicBar': 'Bar', 'JCapsuleChart': 'Bar',
        'JLine': 'Line', 'JSmoothLine': 'Line', 'JStepLine': 'Line',
        'JMultipleLine': 'Line',
        'JMixLineBar': 'MixLineBar', 'DoubleLineBar': 'DoubleLineBar',
        'JPie': 'Pie', 'JRing': 'Ring', 'JRose': 'Rose',
        'JGauge': 'Gauge', 'JColorGauge': 'Gauge', 'JSemiGauge': 'Gauge',
        'JProgress': 'Progress', 'JCustomProgress': 'Progress',
        'JLiquid': 'Liquid', 'JRadialBar': 'RadialBar',
        'JFunnel': 'Funnel', 'JPyramidFunnel': 'Funnel',
        'JRadar': 'Radar', 'JCircleRadar': 'Radar',
        'JScatter': 'Scatter', 'JBubble': 'Bubble',
        'JWordCloud': 'WordCloud',
        'JAreaMap': 'Map', 'JBubbleMap': 'Map', 'JFlyLineMap': 'Map',
        'JHeatMap': 'Map', 'JBarMap': 'Map',
        'JBar3d': '3D', 'JBarGroup3d': '3D',
        'JNumber': 'Number', 'JCountTo': 'CountTo',
        'JTable': 'Table', 'JScrollTable': 'ScrollTable',
        'JPivotTable': 'PivotTable',
        'JScrollRankingBoard': 'Ranking',
        'JText': 'Text', 'JImg': 'Image',
        'JCarousel': 'Carousel', 'JVideoPlay': 'Video',
        'JCustomButton': 'Button', 'JTabs': 'Tabs',
        'JDragBorder': 'Border', 'JDragDecoration': 'Decoration',
        'JIframe': 'Iframe', 'JCurrentTime': 'Time',
    }
    return category_map.get(component, 'Common')


def _deep_merge(base, override):
    """深度合并字典，override 覆盖 base"""
    for k, v in override.items():
        if k in base and isinstance(base[k], dict) and isinstance(v, dict):
            _deep_merge(base[k], v)
        else:
            base[k] = v
    return base
