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 {
  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 statisticOptions = [
  { key: 'minValue', label: '最低值', shortLabel: '低' },
  { key: 'maxValue', label: '最高值', shortLabel: '高' },
  { key: 'avgValue', label: '平均值', shortLabel: '均' },
  { key: 'medianValue', label: '中位数', shortLabel: '中' },
  { key: 'dataCount', label: '数据量', shortLabel: '量' },
] as const;

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

const contentOptions = [
  { key: 'geoLocation', label: '自然地理区位' },
  { key: 'facilityType', label: '设施类别' },
  { key: 'constructionStage', label: '建设阶段' },
  { key: 'planningForm', label: '规划形式' },
] 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 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;
};

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, statisticKey: StatisticKey) {
  if (statisticKey === '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,
      };
    });
}

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 getSeriesValueKey(index: number) {
  return `amount${index}`;
}

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 = node.canClick ? getNodeColor(contentKey, node.id) : undefined;

        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}
                disabled={!node.canClick}
                onClick={() => {
                  if (node.canClick) onSelect(node);
                }}
                title={node.label}
              >
                {node.canClick ? <span className="content-tree-series-mark" style={{ backgroundColor: color }} /> : null}
                <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 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 [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 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 selectedNodeKeys = useMemo(
    () => new Set(selectedContentNodes.map((node) => getSelectionKey(node.contentKey, node.id))),
    [selectedContentNodes],
  );

  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) => {
    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',
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
      },
    });
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }
    return 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 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 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: metricKey,
            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();
    };
  }, [chartQueryVersion, groupKey, metricKey, 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 isCount = statisticKey === 'dataCount';
    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?.[statisticKey] ?? 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: '请选择右侧分类项',
        },
      },
      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} ${selectedStatistic.label}`,
        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, statisticKey) },
            ],
          }),
        },
      })),
      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, statisticKey),
          },
          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,
      },
    };
  }, [chartDataBySelection, metricKey, selectedContentNodes, selectedMetric.label, selectedStatistic.label, 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}>
        <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={() => setActiveContentKey(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>
    </main>
  );
}

export default App;
