import { useEffect, useMemo, useRef, useState } from 'react';
import { AgCharts } from 'ag-charts-react';
import type { AgCartesianChartOptions } from 'ag-charts-community';
import { ModuleRegistry } from 'ag-charts-community';
import { Building2, Construction, LayoutGrid, LocateFixed, MapPinned } from 'lucide-react';
import {
  AnnotationsModule,
  ContextMenuModule,
  CrosshairModule,
  LicenseManager,
  ZoomModule,
} from 'ag-charts-enterprise';
import { AG_CHARTS_LOCALE_ZH_CN } from 'ag-charts-locale';

LicenseManager.setLicenseKey('[v3][RELEASE][0102]_NDg2Njc4MzY3MDgzNw==16d78ca762fb5d2ff740aed081e2af7b');
ModuleRegistry.registerModules([AnnotationsModule, ContextMenuModule, ZoomModule, CrosshairModule]);

const API_BASE_URL = 'https://nest.zwgczx.com/api/v1';
// const API_BASE_URL = 'http://127.0.0.1:9089/api/v1';

const statisticOptions = [
  { key: 'minValue', label: '最低值', shortLabel: '低' },
  { key: 'maxValue', label: '最高值', shortLabel: '高' },
  { key: 'avgValue', label: '平均值', shortLabel: '均' },
  { key: 'medianValue', label: '中位数', shortLabel: '中' },
] as const;

const metricOptions = [
  { key: 'cost', label: '造价（元）' },
  { key: 'buildingArea', label: '建筑面积指标（元/m²）' },
  { key: 'builtArea', label: '建造面积指标（元/m²）' },
  { key: 'usableArea', label: '使用面积指标（元/m²）' },
  { key: 'dataCount', label: '数据量' },
] as const;

const contentOptions = [
  { key: 'geoLocation', label: '自然地理区位' },
  { key: 'facilityType', label: '设施类别' },
  { key: 'constructionStage', label: '建设阶段' },
  { key: 'planningForm', label: '规划形式' },
] as const;

const filterOptions = [
  { key: 'region', label: '省市区', icon: MapPinned },
  { key: 'geoLocation', label: '自然地理区位', icon: LocateFixed },
  { key: 'facilityType', label: '设施类别', icon: Building2 },
  { key: 'constructionStage', label: '建设阶段', icon: Construction },
  { key: 'planningForm', label: '规划形式', icon: LayoutGrid },
] as const;

const browserTreeDefaults = {
  treetype: '256',
  checkStrictly: 'true',
  requestid: '-1',
  workflowid: '181028',
  wfid: '181028',
  billid: '-1812',
  isbill: '1',
  f_weaver_belongto_userid: '267',
  f_weaver_belongto_usertype: '0',
  wf_isagent: '0',
  wf_beagenter: '0',
  wfTestStr: '',
  viewtype: '1',
  fromModule: 'workflow',
  wfCreater: '267',
  disabledConditionCache: 'true',
  companyId: '1',
};

const contentTreeConfigs = {
  geoLocation: {
    endpoint: '/api/public/browser/data/256',
    treeid: '94004',
    fieldid: '305425',
    defaultExpandedLevel: 1,
  },
  facilityType: {
    endpoint: '/api/public/browser/data/256',
    treeid: '94005',
    fieldid: '305426',
    defaultExpandedLevel: 3,
  },
  constructionStage: {
    endpoint: '/api/public/browser/data/256',
    treeid: '94007',
    fieldid: '305428',
    defaultExpandedLevel: 1,
  },
  planningForm: {
    endpoint: '/api/public/browser/data/256',
    treeid: '94006',
    fieldid: '305427',
    defaultExpandedLevel: 1,
  },
} as const;

const chartLineColors = ['#0078a8', '#d14d72', '#1f8f4d', '#d96f23', '#6b5cc8', '#0d7680', '#9a6b12', '#b24b38'];

// const mockGeoLocationPayload = {
//   checkStrictly: true,
//   type: 3,
//   datas: [
//     {
//       allVersionIds: '',
//       canClick: false,
//       checkStrictly: true,
//       dsporder: 0,
//       icon: '',
//       id: '95005_22',
//       isImgIcon: false,
//       isParent: true,
//       linkUrl: '/formmode/search/CustomSearchOpenTree.jsp?pid=95005_22',
//       name: '中国',
//       pid: '0_0',
//       selected: false,
//       title: '中国',
//       type: '2',
//     },
//   ],
//   iconSetting: {
//     bgColor: '#96358a',
//     icon: 'icon-coms-ModelingEngine',
//     fontColor: '#fff',
//   },
// };

type StatisticKey = (typeof statisticOptions)[number]['key'];
type MetricKey = (typeof metricOptions)[number]['key'];
type ContentKey = (typeof contentOptions)[number]['key'];
type FilterKey = (typeof filterOptions)[number]['key'];
type GroupKey = 'year';
type ApiBuildingFunctionStat = {
  group_key?: string | number | null;
  group_name?: string | null;
  min_value?: number | null;
  max_value?: number | null;
  avg_value?: number | null;
  median_value?: number | null;
  data_count?: number | null;
};
type ApiBuildingFunctionStatBatchItem = {
  key?: string;
  data?: ApiBuildingFunctionStat[];
};
type ChartDatum = {
  groupName: string;
  minValue: number | null;
  maxValue: number | null;
  avgValue: number | null;
  medianValue: number | null;
  dataCount: number | null;
};
type TreeNode = {
  id: string;
  label: string;
  children: TreeNode[];
  hasChildren: boolean;
  canClick: boolean;
  expanded: boolean;
  loading: boolean;
  loaded: boolean;
};
type SelectedContentNode = {
  id: string;
  contentKey: ContentKey;
  label: string;
  color: string;
};
type SelectedFilterNode = {
  id: string;
  filterKey: FilterKey;
  label: string;
};

function formatNumber(value: number, maximumFractionDigits: number) {
  return value.toLocaleString('zh-CN', {
    minimumFractionDigits: 0,
    maximumFractionDigits,
  });
}

function formatCostValue(value: number) {
  const absValue = Math.abs(value);
  if (absValue >= 10000) {
    return `${formatNumber(value / 10000, 2)}万元`;
  }
  const fractionDigits = absValue > 0 && absValue < 1 ? 4 : 2;
  return `${formatNumber(value, fractionDigits)}元`;
}

function formatAreaMetricValue(value: number) {
  const absValue = Math.abs(value);
  const fractionDigits = absValue > 0 && absValue < 1 ? 4 : 2;
  return `${formatNumber(value, fractionDigits)}元/m²`;
}

function formatChartValue(value: number, metricKey: MetricKey) {
  if (metricKey === 'dataCount') {
    return formatNumber(value, 0);
  }
  if (metricKey === 'cost') {
    return formatCostValue(value);
  }
  return formatAreaMetricValue(value);
}

function normalizeStat(row: ApiBuildingFunctionStat): ChartDatum {
  return {
    groupName: row.group_name || String(row.group_key ?? '未命名'),
    minValue: row.min_value ?? null,
    maxValue: row.max_value ?? null,
    avgValue: row.avg_value ?? null,
    medianValue: row.median_value ?? null,
    dataCount: row.data_count ?? null,
  };
}

function buildQuery(params: Record<string, string | number | boolean | null | undefined>) {
  const search = new URLSearchParams();
  Object.entries(params).forEach(([key, value]) => {
    if (value == null) return;
    search.set(key, String(value));
  });
  search.set('currenttime', String(Date.now()));
  search.set('__random__', String(Date.now()));
  return search.toString();
}

function pickArray(payload: unknown): unknown[] {
  if (Array.isArray(payload)) return payload;
  if (!payload || typeof payload !== 'object') return [];

  const source = payload as Record<string, unknown>;
  const candidates = [
    source.datas,
    source.data,
    source.data && typeof source.data === 'object' ? (source.data as Record<string, unknown>).datas : null,
    source.data && typeof source.data === 'object' ? (source.data as Record<string, unknown>).list : null,
    source.data && typeof source.data === 'object' ? (source.data as Record<string, unknown>).treeDatas : null,
    source.result,
    source.rows,
    source.list,
    source.treeDatas,
    source.children,
  ];

  for (const candidate of candidates) {
    if (Array.isArray(candidate)) return candidate;
  }
  return [];
}

function readText(row: Record<string, unknown>, keys: string[]) {
  for (const key of keys) {
    const value = row[key];
    if (value !== null && value !== undefined && String(value).trim()) {
      return String(value);
    }
  }
  return '';
}

function normalizeTreeRows(rows: unknown[]): TreeNode[] {
  return rows
    .filter((row): row is Record<string, unknown> => !!row && typeof row === 'object')
    .map((row, index) => {
      const children = normalizeTreeRows(pickArray(row.children ?? row.childs ?? row.subs));
      const id = readText(row, ['id', 'key', 'value', 'nodeid', 'treeid', 'browserid']) || `node-${index}`;
      const label = readText(row, ['name', 'label', 'title', 'text', 'showname', 'showName', 'browsername', 'displayName']) || id;
      const hasChildren =
        children.length > 0 ||
        row.hasChild === true ||
        row.haschild === true ||
        row.isParent === true ||
        row.isparent === true ||
        row.children === true ||
        row.child === true;
      const canClick = row.canClick === true || row.canClick === 'true' || row.canclick === true || row.canclick === 'true';

      return {
        id,
        label,
        children,
        hasChildren,
        canClick,
        expanded: children.length > 0,
        loading: false,
        loaded: children.length > 0,
      };
    });
}

const regionFieldKeys = {
  provinceId: ['provinceId', 'province_id', 'provinceid', 'sfid', 'sf_id', 'shengId', 'sheng_id', 'sheng'],
  provinceName: ['provinceName', 'province_name', 'province', 'sfmc', 'sf', 'shengName', 'sheng_name', 'shengmc'],
  cityId: ['cityId', 'city_id', 'cityid', 'sid', 's_id', 'shiId', 'shi_id', 'shi'],
  cityName: ['cityName', 'city_name', 'city', 'smc', 's', 'shiName', 'shi_name', 'shimc'],
  districtId: ['districtId', 'district_id', 'districtid', 'countyId', 'county_id', 'qid', 'q_id', 'xid', 'x_id', 'id'],
  districtName: ['districtName', 'district_name', 'district', 'countyName', 'county_name', 'county', 'qmc', 'q', 'xmc', 'x', 'name', 'shortname'],
};

function createFilterTreeNode(id: string, label: string, children: TreeNode[] = [], expanded = false): TreeNode {
  return {
    id,
    label,
    children,
    hasChildren: children.length > 0,
    canClick: true,
    expanded,
    loading: false,
    loaded: true,
  };
}

function normalizeFlatRegionRows(rows: unknown[]): TreeNode[] {
  const sourceRows = rows.filter((row): row is Record<string, unknown> => !!row && typeof row === 'object');
  const hasRegionShape = sourceRows.some((row) =>
    readText(row, regionFieldKeys.provinceName) &&
    readText(row, regionFieldKeys.cityName) &&
    readText(row, regionFieldKeys.districtName),
  );
  if (!hasRegionShape) return [];

  const provinceMap = new Map<string, {
    id: string;
    label: string;
    cityMap: Map<string, {
      id: string;
      label: string;
      districts: Map<string, TreeNode>;
    }>;
  }>();

  sourceRows.forEach((row) => {
    const provinceLabel = readText(row, regionFieldKeys.provinceName);
    const cityLabel = readText(row, regionFieldKeys.cityName);
    const districtLabel = readText(row, regionFieldKeys.districtName);
    if (!provinceLabel || !cityLabel || !districtLabel) return;

    const provinceId = `region:province:${readText(row, regionFieldKeys.provinceId) || provinceLabel}`;
    const cityId = `region:city:${provinceLabel}:${readText(row, regionFieldKeys.cityId) || cityLabel}`;
    const districtId = `region:district:${readText(row, regionFieldKeys.districtId) || `${provinceLabel}:${cityLabel}:${districtLabel}`}`;

    const province = provinceMap.get(provinceId) ?? {
      id: provinceId,
      label: provinceLabel,
      cityMap: new Map<string, { id: string; label: string; districts: Map<string, TreeNode> }>(),
    };
    const city = province.cityMap.get(cityId) ?? {
      id: cityId,
      label: cityLabel,
      districts: new Map<string, TreeNode>(),
    };

    city.districts.set(districtId, createFilterTreeNode(districtId, districtLabel));
    province.cityMap.set(cityId, city);
    provinceMap.set(provinceId, province);
  });

  return Array.from(provinceMap.values()).map((province) => {
    const cityNodes = Array.from(province.cityMap.values()).map((city) =>
      createFilterTreeNode(city.id, city.label, Array.from(city.districts.values()), false),
    );
    return createFilterTreeNode(province.id, province.label, cityNodes, true);
  });
}

function updateNode(nodes: TreeNode[], nodeId: string, updater: (node: TreeNode) => TreeNode): TreeNode[] {
  return nodes.map((node) => {
    if (node.id === nodeId) return updater(node);
    return {
      ...node,
      children: updateNode(node.children, nodeId, updater),
    };
  });
}

function getSelectionKey(contentKey: ContentKey, nodeId: string) {
  return `${contentKey}:${nodeId}`;
}

function getFilterSelectionKey(filterKey: FilterKey, nodeId: string) {
  return `${filterKey}:${nodeId}`;
}

function getSeriesValueKey(index: number) {
  return `amount${index}`;
}

function isContentFilterKey(filterKey: FilterKey): filterKey is ContentKey {
  return filterKey !== 'region';
}

function filterTreeNodesByKeyword(nodes: TreeNode[], keyword: string): TreeNode[] {
  const normalizedKeyword = keyword.trim().toLowerCase();
  if (!normalizedKeyword) return nodes;

  const expandDescendants = (targetNodes: TreeNode[]): TreeNode[] => targetNodes.map((node) => ({
    ...node,
    children: expandDescendants(node.children),
    expanded: node.hasChildren || node.children.length > 0 ? true : node.expanded,
  }));

  return nodes.flatMap((node) => {
    const matched = node.label.toLowerCase().includes(normalizedKeyword);
    const children = filterTreeNodesByKeyword(node.children, keyword);
    if (!matched && children.length === 0) return [];
    return [{
      ...node,
      children: matched ? expandDescendants(node.children) : children,
      expanded: true,
    }];
  });
}

function renderTreeNodes(
  nodes: TreeNode[],
  contentKey: ContentKey,
  selectedNodeKeys: Set<string>,
  getNodeColor: (contentKey: ContentKey, nodeId: string) => string,
  onToggle: (nodeId: string) => void,
  onSelect: (node: TreeNode) => void,
  depth = 0,
) {
  return (
    <ul className="content-tree-list" role={depth === 0 ? 'tree' : 'group'}>
      {nodes.map((node) => {
        const selected = selectedNodeKeys.has(getSelectionKey(contentKey, node.id));
        const color = getNodeColor(contentKey, node.id);

        return (
          <li className="content-tree-node" role="treeitem" aria-expanded={node.hasChildren ? node.expanded : undefined} key={node.id}>
            <div className="content-tree-row" style={{ paddingLeft: 8 + depth * 18 }}>
              {node.hasChildren ? (
                <button className="content-tree-caret" type="button" aria-label={node.expanded ? '收起' : '展开'} onClick={() => onToggle(node.id)}>
                  {node.expanded ? '▾' : '▸'}
                </button>
              ) : (
                <span className="content-tree-caret is-leaf" />
              )}
              <button
                className="content-tree-select"
                type="button"
                aria-pressed={selected}
                onClick={() => onSelect(node)}
                title={node.label}
              >
                <span className="content-tree-series-mark" style={{ backgroundColor: color }} />
                <span className="content-tree-label">{node.label}</span>
              </button>
              {node.loading ? <span className="content-tree-loading">加载中</span> : null}
            </div>
            {node.expanded && node.children.length > 0
              ? renderTreeNodes(node.children, contentKey, selectedNodeKeys, getNodeColor, onToggle, onSelect, depth + 1)
              : null}
          </li>
        );
      })}
    </ul>
  );
}

function renderFilterTreeNodes(
  nodes: TreeNode[],
  filterKey: FilterKey,
  selectedNodeKeys: Set<string>,
  onToggle: (nodeId: string) => void,
  onSelect: (node: TreeNode) => void,
  depth = 0,
) {
  return (
    <ul className="content-tree-list filter-tree-list" role={depth === 0 ? 'tree' : 'group'}>
      {nodes.map((node) => {
        const selected = selectedNodeKeys.has(getFilterSelectionKey(filterKey, node.id));

        return (
          <li className="content-tree-node" role="treeitem" aria-expanded={node.hasChildren ? node.expanded : undefined} key={node.id}>
            <div className="content-tree-row filter-tree-row" style={{ paddingLeft: 8 + depth * 18 }}>
              {node.hasChildren ? (
                <button className="content-tree-caret" type="button" aria-label={node.expanded ? '收起' : '展开'} onClick={() => onToggle(node.id)}>
                  {node.expanded ? '▾' : '▸'}
                </button>
              ) : (
                <span className="content-tree-caret is-leaf" />
              )}
              <button
                className="content-tree-select filter-tree-select"
                type="button"
                aria-pressed={selected}
                onClick={() => onSelect(node)}
                title={node.label}
              >
                <span className="filter-tree-check" aria-hidden="true" />
                <span className="content-tree-label">{node.label}</span>
              </button>
              {node.loading ? <span className="content-tree-loading">加载中</span> : null}
            </div>
            {node.expanded && node.children.length > 0
              ? renderFilterTreeNodes(node.children, filterKey, selectedNodeKeys, onToggle, onSelect, depth + 1)
              : null}
          </li>
        );
      })}
    </ul>
  );
}

function App() {
  const workspaceRef = useRef<HTMLElement>(null);
  const chartFrameRef = useRef<HTMLDivElement>(null);
  const treeInitialLoadStartedRef = useRef<Record<ContentKey, boolean>>({
    geoLocation: false,
    facilityType: false,
    constructionStage: false,
    planningForm: false,
  });
  const filterTreeInitialLoadStartedRef = useRef<Record<FilterKey, boolean>>({
    region: false,
    geoLocation: false,
    facilityType: false,
    constructionStage: false,
    planningForm: false,
  });
  const [statisticKey, setStatisticKey] = useState<StatisticKey>('avgValue');
  const [metricKey, setMetricKey] = useState<MetricKey>('cost');
  const [groupKey, setGroupKey] = useState<GroupKey>('year');
  const [statisticMenuOpen, setStatisticMenuOpen] = useState(false);
  const [metricMenuOpen, setMetricMenuOpen] = useState(false);
  const [activeContentKey, setActiveContentKey] = useState<ContentKey>('geoLocation');
  const [treeByContent, setTreeByContent] = useState<Record<ContentKey, TreeNode[]>>({
    geoLocation: [],
    facilityType: [],
    constructionStage: [],
    planningForm: [],
  });
  const [treeLoadingByContent, setTreeLoadingByContent] = useState<Record<ContentKey, boolean>>({
    geoLocation: false,
    facilityType: false,
    constructionStage: false,
    planningForm: false,
  });
  const [treeErrorByContent, setTreeErrorByContent] = useState<Record<ContentKey, string | null>>({
    geoLocation: null,
    facilityType: null,
    constructionStage: null,
    planningForm: null,
  });
  const [selectedContentNodes, setSelectedContentNodes] = useState<SelectedContentNode[]>([]);
  const [chartDataBySelection, setChartDataBySelection] = useState<Record<string, ChartDatum[]>>({});
  const [chartQueryVersion, setChartQueryVersion] = useState(0);
  const [loading, setLoading] = useState(false);
  const [loadError, setLoadError] = useState<string | null>(null);
  const [loadingHint, setLoadingHint] = useState('');
  const [filterTreeByKey, setFilterTreeByKey] = useState<Record<FilterKey, TreeNode[]>>({
    region: [],
    geoLocation: [],
    facilityType: [],
    constructionStage: [],
    planningForm: [],
  });
  const [filterTreeLoadingByKey, setFilterTreeLoadingByKey] = useState<Record<FilterKey, boolean>>({
    region: false,
    geoLocation: false,
    facilityType: false,
    constructionStage: false,
    planningForm: false,
  });
  const [filterTreeErrorByKey, setFilterTreeErrorByKey] = useState<Record<FilterKey, string | null>>({
    region: null,
    geoLocation: null,
    facilityType: null,
    constructionStage: null,
    planningForm: null,
  });
  const [filterSearchTreeByKey, setFilterSearchTreeByKey] = useState<Record<FilterKey, TreeNode[]>>({
    region: [],
    geoLocation: [],
    facilityType: [],
    constructionStage: [],
    planningForm: [],
  });
  const [filterSearchLoadingByKey, setFilterSearchLoadingByKey] = useState<Record<FilterKey, boolean>>({
    region: false,
    geoLocation: false,
    facilityType: false,
    constructionStage: false,
    planningForm: false,
  });
  const [filterSearchErrorByKey, setFilterSearchErrorByKey] = useState<Record<FilterKey, string | null>>({
    region: null,
    geoLocation: null,
    facilityType: null,
    constructionStage: null,
    planningForm: null,
  });
  const [appliedFilters, setAppliedFilters] = useState<Record<FilterKey, SelectedFilterNode[]>>({
    region: [],
    geoLocation: [],
    facilityType: [],
    constructionStage: [],
    planningForm: [],
  });
  const [filterModalKey, setFilterModalKey] = useState<FilterKey | null>(null);
  const [draftFilterNodes, setDraftFilterNodes] = useState<SelectedFilterNode[]>([]);
  const [filterSearchValue, setFilterSearchValue] = useState('');
  const filterSearchComposingRef = useRef(false);
  const filterSearchTimerRef = useRef<number | null>(null);
  const filterSearchRequestSeqRef = useRef<Record<FilterKey, number>>({
    region: 0,
    geoLocation: 0,
    facilityType: 0,
    constructionStage: 0,
    planningForm: 0,
  });
  const lastFilterSearchRef = useRef('');

  const selectedStatistic = statisticOptions.find((option) => option.key === statisticKey) ?? statisticOptions[0];
  const selectedMetric = metricOptions.find((option) => option.key === metricKey) ?? metricOptions[0];
  const activeContent = contentOptions.find((option) => option.key === activeContentKey) ?? contentOptions[0];
  const activeTree = treeByContent[activeContentKey];
  const activeFilter = filterOptions.find((option) => option.key === filterModalKey);
  const activeFilterTree = filterModalKey ? filterTreeByKey[filterModalKey] : [];
  const trimmedFilterSearchValue = filterSearchValue.trim();
  const activeFilterDisplayTree = filterModalKey && trimmedFilterSearchValue ? filterSearchTreeByKey[filterModalKey] : activeFilterTree;
  const activeFilterTreeLoading = filterModalKey
    ? trimmedFilterSearchValue ? filterSearchLoadingByKey[filterModalKey] : filterTreeLoadingByKey[filterModalKey]
    : false;
  const activeFilterTreeError = filterModalKey
    ? trimmedFilterSearchValue ? filterSearchErrorByKey[filterModalKey] : filterTreeErrorByKey[filterModalKey]
    : null;
  const activeFilterCount = Object.values(appliedFilters).reduce((total, nodes) => total + nodes.length, 0);
  const chartEmptyText = selectedContentNodes.length === 0
    ? '请选择右侧分类项'
    : activeFilterCount > 0
      ? '当前筛选无数据'
      : '所选分类暂无数据';
  const selectedValueKey = metricKey === 'dataCount' ? 'dataCount' : statisticKey;
  const requestMetricKey = metricKey === 'dataCount' ? 'cost' : metricKey;
  const seriesValueLabel = metricKey === 'dataCount' ? selectedMetric.label : selectedStatistic.label;
  const selectedNodeKeys = useMemo(
    () => new Set(selectedContentNodes.map((node) => getSelectionKey(node.contentKey, node.id))),
    [selectedContentNodes],
  );
  const draftFilterNodeKeys = useMemo(
    () => new Set(draftFilterNodes.map((node) => getFilterSelectionKey(node.filterKey, node.id))),
    [draftFilterNodes],
  );
  const appliedFilterPayload = useMemo(
    () => filterOptions
      .map((option) => ({
        key: option.key,
        nodes: appliedFilters[option.key].map((node) => ({ nodeId: node.id })),
      }))
      .filter((filter) => filter.nodes.length > 0),
    [appliedFilters],
  );

  const getNodeColor = (contentKey: ContentKey, nodeId: string) => {
    let hash = 0;
    const key = getSelectionKey(contentKey, nodeId);
    for (let index = 0; index < key.length; index += 1) {
      hash = (hash * 31 + key.charCodeAt(index)) % chartLineColors.length;
    }
    return chartLineColors[hash];
  };

  const fetchContentTree = async (contentKey: ContentKey, nodeId?: string, signal?: AbortSignal) => {
    const config = contentTreeConfigs[contentKey];
    if (!config) {
      throw new Error('接口待接入');
    }
    const treeParams = {
      ...browserTreeDefaults,
      treeid: config.treeid,
      cube_treeid: config.treeid,
      fieldid: config.fieldid,
    };
    const params = nodeId
      ? {
          ...treeParams,
          type: '2',
          id: nodeId,
          isVirtual: '',
        }
      : {
          ...treeParams,
          pageSize: '10',
          current: '1',
          min: '1',
          max: '10',
        };
    const response = await fetch(`${config.endpoint}?${buildQuery(params)}`, {
      credentials: 'include',
      signal,
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
      },
    });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const rows = pickArray(await response.json());
    return normalizeTreeRows(rows);
  };

  const normalizeBackendTree = (nodes: TreeNode[]): TreeNode[] => nodes.map((node) => ({
    ...node,
    hasChildren: node.children.length > 0 || node.hasChildren,
    expanded: node.children.length > 0,
    loaded: true,
    children: normalizeBackendTree(node.children),
  }));

  const getTreeNodePrefix = (nodes: TreeNode[]) => {
    const stack = [...nodes];
    while (stack.length) {
      const node = stack.shift();
      if (!node) {
        continue;
      }
      const matched = node.id.match(/^(.*_)\d+$/);
      if (matched) {
        return matched[1];
      }
      stack.push(...node.children);
    }
    return '';
  };

  const fetchRegionFilterTree = async (signal?: AbortSignal) => {
    const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostFilterTree?${buildQuery({ key: 'region' })}`, {
      signal,
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
      },
    });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    const rows = pickArray(await response.json());
    const flatRegionTree = normalizeFlatRegionRows(rows);
    return flatRegionTree.length > 0 ? flatRegionTree : normalizeBackendTree(normalizeTreeRows(rows));
  };

  const fetchBackendFilterTreeSearch = async (filterKey: FilterKey, keyword: string, signal?: AbortSignal) => {
    const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostFilterTreeSearch?${buildQuery({
      key: filterKey,
      keyword,
      nodePrefix: isContentFilterKey(filterKey) ? getTreeNodePrefix(filterTreeByKey[filterKey]) : '',
    })}`, {
      signal,
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
      },
    });
    if (!response.ok) {
      const errorText = await response.text().catch(() => '');
      throw new Error(`搜索接口请求失败：HTTP ${response.status}${errorText ? ` ${errorText.slice(0, 160)}` : ''}`);
    }
    return normalizeBackendTree(normalizeTreeRows(pickArray(await response.json())));
  };

  const loadContentTreeWithDefaultExpansion = async (contentKey: ContentKey) => {
    const defaultExpandedLevel = contentTreeConfigs[contentKey]?.defaultExpandedLevel ?? 0;
    const loadChildren = async (nodes: TreeNode[], level: number): Promise<TreeNode[]> => {
      if (level > defaultExpandedLevel) return nodes;

      return Promise.all(
        nodes.map(async (node) => {
          if (!node.hasChildren) return node;

          const children = node.loaded ? node.children : await fetchContentTree(contentKey, node.id);
          return {
            ...node,
            children: await loadChildren(children, level + 1),
            expanded: true,
            loading: false,
            loaded: true,
            hasChildren: node.hasChildren || children.length > 0,
          };
        }),
      );
    };

    return loadChildren(await fetchContentTree(contentKey), 1);
  };

  const loadFilterTree = async (filterKey: FilterKey, keyword?: string, signal?: AbortSignal) => {
    if (filterKey === 'region') {
      const nodes = await fetchRegionFilterTree(signal);
      return keyword?.trim() ? filterTreeNodesByKeyword(nodes, keyword) : nodes;
    }
    if (keyword?.trim()) {
      return fetchBackendFilterTreeSearch(filterKey, keyword, signal);
    }
    return loadContentTreeWithDefaultExpansion(filterKey);
  };

  const toggleContentNode = (nodeId: string) => {
    if (!contentTreeConfigs[activeContentKey]) return;
    const target = activeTree.find((node) => node.id === nodeId);
    const visit = (nodes: TreeNode[]): TreeNode | null => {
      for (const node of nodes) {
        if (node.id === nodeId) return node;
        const matched = visit(node.children);
        if (matched) return matched;
      }
      return null;
    };
    const node = target ?? visit(activeTree);
    if (!node?.hasChildren) return;

    setTreeByContent((current) => ({
      ...current,
      [activeContentKey]: updateNode(current[activeContentKey], nodeId, (node) => ({
        ...node,
        expanded: node.loaded ? !node.expanded : node.expanded,
        loading: node.loaded ? node.loading : true,
      })),
    }));

    if (node.loaded) return;

    const currentContentKey = activeContentKey;
    fetchContentTree(currentContentKey, nodeId)
      .then((children) => {
        setTreeByContent((current) => ({
          ...current,
          [currentContentKey]: updateNode(current[currentContentKey], nodeId, (currentNode) => ({
            ...currentNode,
            children,
            expanded: true,
            loading: false,
            loaded: true,
            hasChildren: currentNode.hasChildren || children.length > 0,
          })),
        }));
      })
      .catch((error) => {
        setTreeByContent((current) => ({
          ...current,
          [currentContentKey]: updateNode(current[currentContentKey], nodeId, (currentNode) => ({
            ...currentNode,
            loading: false,
          })),
        }));
        setTreeErrorByContent((current) => ({
          ...current,
          [currentContentKey]: error instanceof Error ? error.message : '加载失败',
        }));
      });
  };

  const toggleSelectedContentNode = (node: TreeNode) => {
    const contentKey = activeContentKey;
    setSelectedContentNodes((current) => {
      const selectionKey = getSelectionKey(contentKey, node.id);
      const existingIndex = current.findIndex((item) => getSelectionKey(item.contentKey, item.id) === selectionKey);
      if (existingIndex >= 0) {
        setChartDataBySelection((data) => {
          const { [selectionKey]: _removed, ...rest } = data;
          return rest;
        });
        return current.filter((_, index) => index !== existingIndex);
      }

      const color = getNodeColor(contentKey, node.id);
      return [...current, { id: node.id, contentKey, label: node.label, color }];
    });
  };

  const handleActiveContentKeyChange = (nextContentKey: ContentKey) => {
    if (nextContentKey === activeContentKey) return;
    setActiveContentKey(nextContentKey);
    setSelectedContentNodes([]);
    setChartDataBySelection({});
    setLoadError(null);
    setLoadingHint('');
    setLoading(false);
  };

  const ensureFilterTreeLoaded = (filterKey: FilterKey) => {
    if (filterTreeByKey[filterKey].length > 0 || filterTreeInitialLoadStartedRef.current[filterKey]) return;

    filterTreeInitialLoadStartedRef.current[filterKey] = true;
    setFilterTreeLoadingByKey((current) => ({ ...current, [filterKey]: true }));
    setFilterTreeErrorByKey((current) => ({ ...current, [filterKey]: null }));
    loadFilterTree(filterKey)
      .then((nodes) => {
        setFilterTreeByKey((current) => ({ ...current, [filterKey]: nodes }));
      })
      .catch((error) => {
        filterTreeInitialLoadStartedRef.current[filterKey] = false;
        setFilterTreeErrorByKey((current) => ({
          ...current,
          [filterKey]: error instanceof Error ? error.message : '加载失败',
        }));
      })
      .finally(() => {
        setFilterTreeLoadingByKey((current) => ({ ...current, [filterKey]: false }));
      });
  };

  const openFilterModal = (filterKey: FilterKey) => {
    setFilterModalKey(filterKey);
    setDraftFilterNodes(appliedFilters[filterKey]);
    setFilterSearchValue('');
    lastFilterSearchRef.current = '';
    if (filterSearchTimerRef.current != null) {
      window.clearTimeout(filterSearchTimerRef.current);
      filterSearchTimerRef.current = null;
    }
    ensureFilterTreeLoaded(filterKey);
  };

  const closeFilterModal = () => {
    if (filterModalKey) {
      filterSearchRequestSeqRef.current[filterModalKey] += 1;
    }
    if (filterSearchTimerRef.current != null) {
      window.clearTimeout(filterSearchTimerRef.current);
      filterSearchTimerRef.current = null;
    }
    lastFilterSearchRef.current = '';
    setFilterModalKey(null);
    setDraftFilterNodes([]);
    setFilterSearchValue('');
  };

  const scheduleFilterSearch = (filterKey: FilterKey, rawKeyword: string) => {
    if (filterSearchTimerRef.current != null) {
      window.clearTimeout(filterSearchTimerRef.current);
      filterSearchTimerRef.current = null;
    }

    const keyword = rawKeyword.trim();
    const searchKey = `${filterKey}:${keyword}`;
    if (keyword && lastFilterSearchRef.current === searchKey) {
      return;
    }
    lastFilterSearchRef.current = searchKey;
    filterSearchRequestSeqRef.current[filterKey] += 1;
    const requestSeq = filterSearchRequestSeqRef.current[filterKey];

    if (!keyword) {
      setFilterSearchTreeByKey((current) => ({ ...current, [filterKey]: [] }));
      setFilterSearchErrorByKey((current) => ({ ...current, [filterKey]: null }));
      setFilterSearchLoadingByKey((current) => ({ ...current, [filterKey]: false }));
      return;
    }

    filterSearchTimerRef.current = window.setTimeout(() => {
      setFilterSearchLoadingByKey((current) => ({ ...current, [filterKey]: true }));
      setFilterSearchErrorByKey((current) => ({ ...current, [filterKey]: null }));
      void loadFilterTree(filterKey, keyword)
        .then((nodes) => {
          if (filterSearchRequestSeqRef.current[filterKey] !== requestSeq) return;
          setFilterSearchTreeByKey((current) => ({ ...current, [filterKey]: nodes }));
        })
        .catch((error) => {
          if (filterSearchRequestSeqRef.current[filterKey] !== requestSeq) return;
          setFilterSearchErrorByKey((current) => ({
            ...current,
            [filterKey]: error instanceof Error ? error.message : '加载失败',
          }));
        })
        .finally(() => {
          if (filterSearchRequestSeqRef.current[filterKey] !== requestSeq) return;
          setFilterSearchLoadingByKey((current) => ({ ...current, [filterKey]: false }));
        });
    }, 500);
  };

  const toggleFilterTreeNode = (nodeId: string) => {
    if (!filterModalKey) return;
    const visit = (nodes: TreeNode[]): TreeNode | null => {
      for (const node of nodes) {
        if (node.id === nodeId) return node;
        const matched = visit(node.children);
        if (matched) return matched;
      }
      return null;
    };
    const node = visit(filterTreeByKey[filterModalKey]);
    if (!node?.hasChildren) return;

    setFilterTreeByKey((current) => ({
      ...current,
      [filterModalKey]: updateNode(current[filterModalKey], nodeId, (currentNode) => ({
        ...currentNode,
        expanded: currentNode.loaded ? !currentNode.expanded : currentNode.expanded,
        loading: currentNode.loaded ? currentNode.loading : true,
      })),
    }));

    if (node.loaded || !isContentFilterKey(filterModalKey)) return;

    const currentFilterKey = filterModalKey;
    fetchContentTree(currentFilterKey, nodeId)
      .then((children) => {
        setFilterTreeByKey((current) => ({
          ...current,
          [currentFilterKey]: updateNode(current[currentFilterKey], nodeId, (currentNode) => ({
            ...currentNode,
            children,
            expanded: true,
            loading: false,
            loaded: true,
            hasChildren: currentNode.hasChildren || children.length > 0,
          })),
        }));
      })
      .catch((error) => {
        setFilterTreeByKey((current) => ({
          ...current,
          [currentFilterKey]: updateNode(current[currentFilterKey], nodeId, (currentNode) => ({
            ...currentNode,
            loading: false,
          })),
        }));
        setFilterTreeErrorByKey((current) => ({
          ...current,
          [currentFilterKey]: error instanceof Error ? error.message : '加载失败',
        }));
      });
  };

  const toggleDraftFilterNode = (node: TreeNode) => {
    if (!filterModalKey) return;
    const currentFilterKey = filterModalKey;
    setDraftFilterNodes((current) => {
      const selectionKey = getFilterSelectionKey(currentFilterKey, node.id);
      const exists = current.some((item) => getFilterSelectionKey(item.filterKey, item.id) === selectionKey);
      if (exists) {
        return current.filter((item) => getFilterSelectionKey(item.filterKey, item.id) !== selectionKey);
      }
      return [...current, { id: node.id, filterKey: currentFilterKey, label: node.label }];
    });
  };

  const applyFilterModal = () => {
    if (!filterModalKey) return;
    setAppliedFilters((current) => ({
      ...current,
      [filterModalKey]: draftFilterNodes,
    }));
    setChartDataBySelection({});
    setLoadError(null);
    if (selectedContentNodes.length > 0) {
      setLoadingHint('正在按筛选条件重新计算');
      setLoading(true);
    }
    setChartQueryVersion((version) => version + 1);
    closeFilterModal();
  };

  const clearFilter = (filterKey: FilterKey) => {
    if (appliedFilters[filterKey].length === 0) return;
    setAppliedFilters((current) => ({
      ...current,
      [filterKey]: [],
    }));
    setChartDataBySelection({});
    setLoadError(null);
    if (selectedContentNodes.length > 0) {
      setLoadingHint('正在按筛选条件重新计算');
      setLoading(true);
    }
    setChartQueryVersion((version) => version + 1);
  };

  const updateMetricKey = (nextMetricKey: MetricKey) => {
    setMetricKey(nextMetricKey);
    setChartDataBySelection({});
    setLoadError(null);
    setLoadingHint('正在重新加载数据');
    setLoading(true);
    setChartQueryVersion((version) => version + 1);
  };

  useEffect(() => {
    if (!contentTreeConfigs[activeContentKey]) return;
    if (treeByContent[activeContentKey].length > 0 || treeInitialLoadStartedRef.current[activeContentKey]) return;

    const currentContentKey = activeContentKey;
    treeInitialLoadStartedRef.current[currentContentKey] = true;
    setTreeLoadingByContent((current) => ({ ...current, [currentContentKey]: true }));
    setTreeErrorByContent((current) => ({ ...current, [currentContentKey]: null }));

    loadContentTreeWithDefaultExpansion(currentContentKey)
      .then((nodes) => {
        setTreeByContent((current) => ({ ...current, [currentContentKey]: nodes }));
      })
      .catch((error) => {
        treeInitialLoadStartedRef.current[currentContentKey] = false;
        setTreeErrorByContent((current) => ({
          ...current,
          [currentContentKey]: error instanceof Error ? error.message : '加载失败',
        }));
      })
      .finally(() => {
        setTreeLoadingByContent((current) => ({ ...current, [currentContentKey]: false }));
      });
  }, [activeContentKey, treeByContent]);

  useEffect(() => {
    const controller = new AbortController();

    async function loadStats() {
      if (selectedContentNodes.length === 0) {
        setChartDataBySelection({});
        setLoading(false);
        setLoadingHint('');
        setLoadError(null);
        return;
      }

      setLoading(true);
      setLoadingHint('正在加载数据');
      setLoadError(null);
      try {
        const hasAnyMissingNode = selectedContentNodes.some((node) => !chartDataBySelection[getSelectionKey(node.contentKey, node.id)]);
        if (!hasAnyMissingNode) {
          setLoading(false);
          setLoadingHint('');
          return;
        }

        const response = await fetch(`${API_BASE_URL}/zw/getBuildingFunctionCostStatsBatch`, {
          method: 'POST',
          signal: controller.signal,
          headers: {
            'Content-Type': 'application/json',
          },
            body: JSON.stringify({
              groupBy: groupKey,
              metric: requestMetricKey,
              filters: appliedFilterPayload,
              nodes: selectedContentNodes.map((node) => ({
                key: getSelectionKey(node.contentKey, node.id),
                contentKey: node.contentKey,
              nodeId: node.id,
            })),
          }),
        });
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`);
        }
        const payload = (await response.json()) as { data?: ApiBuildingFunctionStatBatchItem[] };
        const results = (payload.data ?? [])
          .filter((item) => item.key)
          .map((item) => [item.key as string, (item.data ?? []).map(normalizeStat).slice(0, 36)] as const);
        setChartDataBySelection((current) => ({
          ...current,
          ...Object.fromEntries(results),
        }));
      } catch (error) {
        if (controller.signal.aborted) return;
        setLoadError(error instanceof Error ? error.message : '接口请求失败');
      } finally {
        if (!controller.signal.aborted) {
          setLoading(false);
          setLoadingHint('');
        }
      }
    }

    void loadStats();

    return () => {
      controller.abort();
    };
  }, [appliedFilterPayload, chartQueryVersion, groupKey, metricKey, requestMetricKey, selectedContentNodes]);

  useEffect(() => {
    const frame = chartFrameRef.current;
    const fullscreenTarget = workspaceRef.current;
    if (!frame || !fullscreenTarget) return;

    const getFullscreenButton = () => frame.querySelector<HTMLButtonElement>('.chart-fullscreen-button');
    const getStatisticButton = () => frame.querySelector<HTMLElement>('.ag-charts-myButton-statistic')?.closest<HTMLButtonElement>('.ag-charts-toolbar__button');
    const syncToolbarButtons = () => {
      const button = getFullscreenButton();
      if (button) {
        let icon = button.querySelector<HTMLElement>('.ag-charts-myButton-fullScreen');
        if (!icon) {
          button.innerHTML = '<i class="anticon anticon-arrow-salt ag-charts-myButton-fullScreen ag-charts-diy-button"></i>';
          icon = button.querySelector<HTMLElement>('.ag-charts-myButton-fullScreen');
        }

        const isFullscreen = document.fullscreenElement === fullscreenTarget;
        button.classList.toggle('ag-charts-toolbar__button--active', isFullscreen);
        icon?.classList.toggle('anticon-arrow-salt', !isFullscreen);
        icon?.classList.toggle('anticon-shrink', isFullscreen);
      }

      const statisticButton = getStatisticButton();
      if (statisticButton) {
        statisticButton.classList.add('chart-statistic-button');
        statisticButton.setAttribute('aria-expanded', String(statisticMenuOpen));
      }
    };

    const toggleFullscreen = () => {
      if (document.fullscreenElement === fullscreenTarget) {
        void document.exitFullscreen();
      } else {
        void fullscreenTarget.requestFullscreen();
      }
    };

    const handleKeyDown = (event: KeyboardEvent) => {
      if (event.key !== 'F11') return;

      event.preventDefault();
      event.stopPropagation();
      toggleFullscreen();
    };

    const handleFullscreenChange = () => {
      syncToolbarButtons();
    };

    const handleToolbarClick = (event: MouseEvent) => {
      const target = event.target as Element | null;
      const button = target?.closest<HTMLButtonElement>(
        '.chart-fullscreen-button, .chart-statistic-button',
      );
      if (!button || !frame.contains(button)) return;

      event.preventDefault();
      event.stopPropagation();
      event.stopImmediatePropagation();
      if (button.classList.contains('chart-statistic-button')) {
        setMetricMenuOpen(false);
        setStatisticMenuOpen((open) => !open);
      } else {
        toggleFullscreen();
      }
    };

    const handleToolbarKeyDown = (event: KeyboardEvent) => {
      if (event.key !== ' ' && event.key !== 'Enter') return;

      const target = event.target as Element | null;
      const button = target?.closest<HTMLButtonElement>(
        '.chart-fullscreen-button, .chart-statistic-button',
      );
      if (!button || !frame.contains(button)) return;

      event.preventDefault();
      event.stopPropagation();
      event.stopImmediatePropagation();
      if (button.classList.contains('chart-statistic-button')) {
        setMetricMenuOpen(false);
        setStatisticMenuOpen((open) => !open);
      } else {
        toggleFullscreen();
      }
    };

    const suppressBrowserContextMenu = (event: MouseEvent) => {
      event.preventDefault();
    };

    const observer = new MutationObserver(syncToolbarButtons);
    document.addEventListener('keydown', handleKeyDown, true);
    document.addEventListener('fullscreenchange', handleFullscreenChange);
    document.addEventListener('contextmenu', suppressBrowserContextMenu);
    frame.addEventListener('contextmenu', suppressBrowserContextMenu);
    frame.addEventListener('click', handleToolbarClick, true);
    frame.addEventListener('keydown', handleToolbarKeyDown, true);
    observer.observe(frame, { childList: true, subtree: true });
    syncToolbarButtons();

    return () => {
      document.removeEventListener('keydown', handleKeyDown, true);
      document.removeEventListener('fullscreenchange', handleFullscreenChange);
      document.removeEventListener('contextmenu', suppressBrowserContextMenu);
      frame.removeEventListener('contextmenu', suppressBrowserContextMenu);
      frame.removeEventListener('click', handleToolbarClick, true);
      frame.removeEventListener('keydown', handleToolbarKeyDown, true);
      observer.disconnect();
    };
  }, [statisticMenuOpen]);

  const chartOptions = useMemo<AgCartesianChartOptions>(() => {
    const groupNames: string[] = [];
    const groupNameSeen = new Set<string>();
    selectedContentNodes.forEach((node) => {
      const rows = chartDataBySelection[getSelectionKey(node.contentKey, node.id)] ?? [];
      rows.forEach((datum) => {
        if (groupNameSeen.has(datum.groupName)) return;
        groupNameSeen.add(datum.groupName);
        groupNames.push(datum.groupName);
      });
    });
    const visibleData = groupNames.map((groupName) => {
      const row: Record<string, string | number | null> = { groupName };
      selectedContentNodes.forEach((node, index) => {
        const datum = chartDataBySelection[getSelectionKey(node.contentKey, node.id)]?.find((item) => item.groupName === groupName);
        row[getSeriesValueKey(index)] = datum?.[selectedValueKey] ?? null;
      });
      return row;
    });

    return {
      theme: {
        palette: {
          fills: ['#006f9b', '#ff7faa', '#00994d', '#ff8833', '#00a0dd'],
          strokes: ['#003f58', '#934962', '#004a25', '#914d1d', '#006288'],
        },
        params: {
          foregroundColor: '#262a33',
          backgroundColor: '#fff1e5',
          accentColor: '#0d7680',
          fontFamily: '"Microsoft YaHei", "PingFang SC", "Segoe UI", Arial, sans-serif',
          fontSize: 14,
          tooltipBackgroundColor: '#fff7ef',
          tooltipTextColor: '#262a33',
        },
      },
      locale: {
        localeText: AG_CHARTS_LOCALE_ZH_CN,
      },
      background: {
        fill: 'transparent',
      },
      padding: {
        top: 16,
        right: 16,
        bottom: 18,
        left: 24,
      },
      data: visibleData,
      zoom: {
        enabled: true,
        anchorPointX: 'pointer',
        anchorPointY: 'pointer',
        buttons: {
          enabled: true,
          visible: 'hover',
          buttons: [
            { icon: 'zoom-out', value: 'zoom-out', section: 'zoom', tooltip: '缩小' },
            { icon: 'zoom-in', value: 'zoom-in', section: 'zoom', tooltip: '放大' },
            { icon: 'pan-left', value: 'pan-left', section: 'pan', tooltip: '左移' },
            { icon: 'pan-right', value: 'pan-right', section: 'pan', tooltip: '右移' },
            { icon: 'reset', value: 'reset', section: 'reset', tooltip: '重置' },
          ],
        },
      },
      contextMenu: {
        enabled: true,
        items: ['defaults'],
      },
      overlays: {
        noData: {
          text: chartEmptyText,
        },
      },
      annotations: {
        enabled: true,
        toolbar: {
          buttons: ([
            {
              value: 'note',
              tooltip: '切换统计指标',
              label: `<span class="ag-charts-myButton-statistic ag-charts-diy-button">${selectedStatistic.shortLabel}</span>`,
            },
            {
              icon: 'trend-line-drawing',
              value: 'line-menu',
              tooltip: 'Line Tool',
            },
            {
              icon: 'text-annotation',
              value: 'text-menu',
              tooltip: 'Text Tool',
            },
            {
              icon: 'arrow-drawing',
              value: 'shape-menu',
              tooltip: 'Shape Tool',
            },
            {
              icon: 'fibonacci-retracement-drawing',
              value: 'fibonacci-menu',
              tooltip: 'Fibonacci Tool',
            },
            {
              icon: 'delete',
              value: 'clear',
              tooltip: 'Clear annotations',
            },
          ] as unknown as NonNullable<NonNullable<AgCartesianChartOptions['annotations']>['toolbar']>['buttons']),
        },
      },
      series: selectedContentNodes.map((node, index) => ({
        type: 'line',
        xKey: 'groupName',
        yKey: getSeriesValueKey(index),
        yName: `${node.label} ${seriesValueLabel}`,
        stroke: node.color,
        strokeWidth: 2,
        marker: {
          enabled: true,
          fill: node.color,
          stroke: node.color,
          size: 5,
        },
        interpolation: {
          type: 'smooth',
        },
        tooltip: {
          renderer: ({ datum, yKey, yName }) => ({
            title: yName,
            data: [
              { label: selectedMetric.label, value: formatChartValue(Number(datum[yKey]), metricKey) },
            ],
          }),
        },
      })),
      axes: {
        x: {
          type: 'category',
          position: 'bottom',
          line: {
            enabled: true,
            stroke: '#c8b9a7',
          },
          tick: {
            enabled: false,
          },
          label: {
            color: '#1f2933',
            fontSize: 12,
          },
          crosshair: {
            snap: false,
          },
        },
        y: {
          type: 'number',
          position: 'left',
          title: {
            text: '',
          },
          label: {
            color: '#1f2933',
            fontSize: 12,
            formatter: ({ value }) => formatChartValue(Number(value), metricKey),
          },
          line: {
            enabled: false,
          },
          tick: {
            enabled: false,
          },
          gridLine: {
            enabled: true,
            style: [
              {
                stroke: '#e5d9ca',
                lineDash: [0],
              },
            ],
          },
          crosshair: {
            snap: false,
          },
        },
      },
      legend: {
        enabled: selectedContentNodes.length > 1,
      },
      tooltip: {
        enabled: true,
        mode: 'shared',
        pagination: true,
      },
    };
  }, [activeFilterCount, chartDataBySelection, chartEmptyText, metricKey, requestMetricKey, selectedContentNodes, selectedMetric.label, seriesValueLabel, selectedValueKey, statisticKey]);

  return (
    <main className="dashboard-shell">
      <div className="watermark-layer" aria-hidden="true">
        {Array.from({ length: 18 }).map((_, index) => (
          <span key={index}>众为数字化管理平台</span>
        ))}
      </div>

      <section className="workspace" aria-label="年度费用模板" ref={workspaceRef}>
        <div className="chart-filter-bar chart-filter-bar--workspace" aria-label="筛选条件">
          {filterOptions.map((option) => {
            const count = appliedFilters[option.key].length;
            const FilterIcon = option.icon;
            return (
              <button
                className="chart-filter-button"
                type="button"
                key={option.key}
                aria-pressed={count > 0}
                title={count > 0 ? `${option.label}：已选${count}项` : option.label}
                onClick={() => openFilterModal(option.key)}
              >
                <FilterIcon className="chart-filter-icon" aria-hidden="true" strokeWidth={2} />
                <span>{option.label}</span>
                {count > 0 ? <strong>{count}</strong> : null}
              </button>
            );
          })}
          {activeFilterCount > 0 ? (
            <button
              className="chart-filter-clear"
              type="button"
              title="清空全部筛选"
              onClick={() => {
                setAppliedFilters({
                  region: [],
                  geoLocation: [],
                  facilityType: [],
                  constructionStage: [],
                  planningForm: [],
                });
                setChartDataBySelection({});
                setLoadError(null);
                if (selectedContentNodes.length > 0) {
                  setLoadingHint('正在按筛选条件重新计算');
                  setLoading(true);
                }
                setChartQueryVersion((version) => version + 1);
              }}
            >
              清空
            </button>
          ) : null}
        </div>
        <section className="chart-area" aria-label="年度总费用图表">
          <div className="chart-frame" ref={chartFrameRef}>
            {statisticMenuOpen ? (
              <div className="statistic-switcher-menu" role="menu" aria-label="切换统计指标">
                {statisticOptions.map((option) => (
                  <button
                    className="statistic-switcher-menu-item"
                    type="button"
                    role="menuitem"
                    key={option.key}
                    aria-current={option.key === statisticKey}
                    onClick={() => {
                      setStatisticKey(option.key);
                      setStatisticMenuOpen(false);
                    }}
                  >
                    {option.label}
                  </button>
                ))}
              </div>
            ) : null}
            <div className="metric-switcher">
              <button
                className="metric-switcher-button"
                type="button"
                title="切换纵坐标指标"
                aria-expanded={metricMenuOpen}
                aria-haspopup="menu"
                aria-label={`纵坐标：${selectedMetric.label}`}
                onClick={() => {
                  setStatisticMenuOpen(false);
                  setMetricMenuOpen((open) => !open);
                }}
              >
                {selectedMetric.label}
              </button>
              {metricMenuOpen ? (
                <div className="metric-switcher-menu" role="menu" aria-label="切换纵坐标指标">
                  {metricOptions.map((option) => (
                    <button
                      className="metric-switcher-menu-item"
                      type="button"
                      role="menuitem"
                      key={option.key}
                      aria-current={option.key === metricKey}
                      onClick={() => {
                        updateMetricKey(option.key);
                        setMetricMenuOpen(false);
                      }}
                    >
                      {option.label}
                    </button>
                  ))}
                </div>
              ) : null}
            </div>
            {loading || loadError ? <div className="chart-status">{loading ? loadingHint || '加载中' : loadError}</div> : null}
            <button className="chart-fullscreen-button ag-charts-toolbar__button" type="button" title="全屏(F11)">
              <i className="anticon anticon-arrow-salt ag-charts-myButton-fullScreen ag-charts-diy-button" />
            </button>
            <AgCharts options={chartOptions} />
            {loading ? (
              <div className="chart-loading-mask" aria-live="polite" aria-busy="true">
                <div className="chart-loading-panel">{loadingHint || '加载中'}</div>
              </div>
            ) : null}
          </div>
        </section>

        <aside className="right-panel" aria-label="选择内容">
          <div className="content-tabs" role="tablist" aria-label="选择内容切换项">
            {contentOptions.map((option) => (
              <button
                className="content-tab"
                type="button"
                role="tab"
                key={option.key}
                aria-selected={option.key === activeContentKey}
                onClick={() => handleActiveContentKeyChange(option.key)}
              >
                {option.label}
              </button>
            ))}
          </div>
          <div className="content-tree-panel">
            <div className="content-tree-title">{activeContent.label}</div>
            {treeLoadingByContent[activeContentKey] ? (
              <div className="content-tree-empty">加载中</div>
            ) : treeErrorByContent[activeContentKey] ? (
              <div className="content-tree-empty">{treeErrorByContent[activeContentKey]}</div>
            ) : activeTree.length > 0 ? (
              renderTreeNodes(activeTree, activeContentKey, selectedNodeKeys, getNodeColor, toggleContentNode, toggleSelectedContentNode)
            ) : (
              <div className="content-tree-empty">接口待接入</div>
            )}
          </div>
        </aside>
      </section>
      {filterModalKey && activeFilter ? (
        <div className="filter-modal-backdrop" role="presentation" onMouseDown={closeFilterModal}>
          <section
            className="filter-modal"
            role="dialog"
            aria-modal="true"
            aria-label={`${activeFilter.label}筛选`}
            onMouseDown={(event) => event.stopPropagation()}
          >
            <header className="filter-modal-header">
              <h2>{activeFilter.label}</h2>
              <button className="filter-modal-close" type="button" aria-label="关闭" onClick={closeFilterModal}>×</button>
            </header>
            <div className="filter-modal-search">
              <input
                type="search"
                value={filterSearchValue}
                placeholder="搜索"
                onChange={(event) => {
                  const nextValue = event.target.value;
                  setFilterSearchValue(nextValue);
                  if (!filterSearchComposingRef.current && filterModalKey) {
                    scheduleFilterSearch(filterModalKey, nextValue);
                  }
                }}
                onCompositionStart={() => {
                  filterSearchComposingRef.current = true;
                }}
                onCompositionEnd={(event) => {
                  filterSearchComposingRef.current = false;
                  const nextValue = event.currentTarget.value;
                  setFilterSearchValue(nextValue);
                  if (filterModalKey) {
                    scheduleFilterSearch(filterModalKey, nextValue);
                  }
                }}
              />
            </div>
            <div className="filter-modal-selected">
              {draftFilterNodes.length > 0 ? `已选 ${draftFilterNodes.length} 项` : '未选择'}
            </div>
            <div className="filter-modal-tree">
              {activeFilterTreeLoading ? (
                <div className="content-tree-empty">加载中</div>
              ) : activeFilterTreeError ? (
                <div className="content-tree-empty">{activeFilterTreeError}</div>
              ) : activeFilterDisplayTree.length > 0 ? (
                renderFilterTreeNodes(activeFilterDisplayTree, filterModalKey, draftFilterNodeKeys, toggleFilterTreeNode, toggleDraftFilterNode)
              ) : (
                <div className="content-tree-empty">{trimmedFilterSearchValue ? '无匹配结果' : '暂无数据'}</div>
              )}
            </div>
            <footer className="filter-modal-actions">
              <button className="filter-modal-clear" type="button" onClick={() => setDraftFilterNodes([])}>
                清空当前
              </button>
              <button className="filter-modal-cancel" type="button" onClick={closeFilterModal}>
                取消
              </button>
              <button className="filter-modal-confirm" type="button" onClick={applyFilterModal}>
                确认
              </button>
            </footer>
          </section>
        </div>
      ) : null}
    </main>
  );
}

export default App;
