(() => {
  "use strict";

  let config =
    window.MODULE_FORM_CONFIG ||
    (window.PROGRAM_FORM_CONFIGS && window.PROGRAM_FORM_CONFIGS[window.MODULE_PROGRAM_ID]);
  const state = { lastResult: null, lastParams: null, dependency: null, busy: false, revision: 0, sources: {} };

  function finiteNumber(value) {
    return value === null || value === undefined || String(value).trim() === "" ? NaN : Number(value);
  }

  async function loadCanonicalInputSchema() {
    const bridge = api();
    if (!config || !bridge || typeof bridge.get_program_input_schema !== "function") return;
    try {
      const payload = await bridge.get_program_input_schema(config.programId);
      const fields = payload && payload.schema && payload.schema.fields;
      if (Array.isArray(fields) && fields.length) config = { ...config, fields };
    } catch (error) {
      console.warn("program input schema bridge failed", error);
    }
  }

  function segmentedLines(points, project, color, width = 2.8) {
    const segments = [];
    let segment = [];
    for (const point of points) {
      if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) {
        if (segment.length) segments.push(segment);
        segment = [];
      } else {
        const p = project(point);
        segment.push(`${p.x.toFixed(2)},${p.y.toFixed(2)}`);
      }
    }
    if (segment.length) segments.push(segment);
    return segments.map(s => `<polyline fill="none" stroke="${escapeHtml(color)}" stroke-width="${width}" vector-effect="non-scaling-stroke" points="${s.join(' ')}"/>`).join('');
  }

  function downloadFile(text, name, type) {
    const url = URL.createObjectURL(new Blob([text], { type }));
    const a = document.createElement('a');
    a.href = url; a.download = name; a.click();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  function installFigureTools() {
    document.querySelectorAll('.result svg.chart, .result svg.section-chart').forEach((svg, index) => {
      const bar = document.createElement('div');
      bar.className = 'figure-tools';
      bar.innerHTML = '<button type="button" data-enlarge>放大图表</button><button type="button" data-save>保存SVG</button>';
      svg.insertAdjacentElement('beforebegin', bar);
      bar.querySelector('[data-save]').addEventListener('click', () => {
        const copy = svg.cloneNode(true);
        copy.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
        copy.style.cssText = 'font-family:Times New Roman,KaiTi,楷体,serif;background:white;color:#173b53';
        downloadFile(new XMLSerializer().serializeToString(copy), `${config.programId}_图${index+1}.svg`, 'image/svg+xml');
      });
      bar.querySelector('[data-enlarge]').addEventListener('click', () => {
        const dialog = document.createElement('dialog');
        dialog.className = 'figure-dialog';
        const close = document.createElement('button');
        close.textContent = '关闭大图';
        close.addEventListener('click', () => dialog.close());
        dialog.append(close, svg.cloneNode(true));
        dialog.addEventListener('close', () => dialog.remove());
        document.body.append(dialog); dialog.showModal();
      });
    });
    document.querySelectorAll('.result table').forEach((table, index) => {
      const copyButton = document.createElement('button');
      copyButton.type = 'button'; copyButton.className = 'icon-button table-copy';
      copyButton.title = '复制表格到 Excel'; copyButton.setAttribute('aria-label', copyButton.title);
      copyButton.innerHTML = '<i data-lucide="copy"></i>';
      table.parentElement.insertAdjacentElement('beforebegin', copyButton);
      copyButton.addEventListener('click', async () => {
        const cells = [...table.rows].map(row => [...row.cells].map(cell => {
          let value = cell.textContent.replace(/\r?\n|\t/g, ' ');
          if (/^[=+@]/.test(value) || (/^-/.test(value) && !Number.isFinite(Number(value)))) value = "'" + value;
          return value;
        }));
        const text = cells.map(row => row.join('\t')).join('\r\n');
        const html = '<table>' + cells.map((row, index) => '<tr>' + row.map(value =>
          `<${index ? 'td' : 'th'}>${escapeHtml(value)}</${index ? 'td' : 'th'}>`).join('') + '</tr>').join('') + '</table>';
        try {
          if (navigator.clipboard?.write && window.ClipboardItem) {
            await navigator.clipboard.write([new ClipboardItem({
              'text/plain': new Blob([text], {type:'text/plain'}),
              'text/html': new Blob([html], {type:'text/html'}),
            })]);
          } else {
            await navigator.clipboard.writeText(text);
          }
          setNotice('表格已复制，可直接粘贴到 Excel。', false);
        } catch (error) {
          const range = document.createRange(); range.selectNodeContents(table);
          const selection = window.getSelection(); selection.removeAllRanges(); selection.addRange(range);
          setNotice('剪贴板访问受限，已选中表格，请按 Ctrl+C 复制。', false);
        }
      });
      const button = document.createElement('button');
      button.type = 'button'; button.className = 'table-download'; button.textContent = '导出此表 CSV';
      table.parentElement.insertAdjacentElement('beforebegin', button);
      button.addEventListener('click', () => {
        const rows = [...table.rows].map(row => [...row.cells].map(cell => {
          let text = cell.textContent;
          if (/^[=+@]/.test(text) || /^-[^\d.]/.test(text)) text = "'" + text;
          return '"' + text.replace(/"/g, '""') + '"';
        }).join(',')).join('\r\n');
        downloadFile('\uFEFF' + rows, `${config.programId}_表${index+1}.csv`, 'text/csv;charset=utf-8');
      });
    });
    window.lucide?.createIcons();
  }

  function invalidateResult() {
    state.revision += 1;
    if (state.lastResult) setNotice("参数已修改，请重新计算后再导出。", false);
    state.lastResult = null;
    state.lastParams = null;
    setResultActive(false);
    document.querySelectorAll('[data-action="export"], [data-action="generate-book"], [data-action="export-cad-section"]').forEach(b => b.disabled = true);
    window.dispatchEvent(new CustomEvent("calculation-invalidated"));
  }

  function applyMethodConditions() {
    const value = key => document.querySelector(`[name="${CSS.escape(key)}"]`)?.value;
    for (const field of config.fields) {
      let active = true;
      if (config.programId === '1-13') {
        if (field.key === 'cs_fixed') active = value('cs_method') === 'fixed';
        if (['cs_cv_ratio','fit_cs_cv_ratio'].includes(field.key)) active = value('cs_method') !== 'fixed';
      }
      if (config.programId === '1-09') {
        const mean = value('rainfall_is_mean') === 'true';
        if (['cv','rainfall_mean_mm'].includes(field.key)) active = mean;
        if (/^rainfall_\d+$/.test(field.key)) active = !mean;
      }
      if (config.programId === '1-11' && /^station_\d+_/.test(field.key)) {
        const index = Number(field.key.split('_')[1]);
        active = !value('stations_json') && Number(value('stations_count')) >= index;
      }
      const el = document.querySelector(`[name="${CSS.escape(field.key)}"]`);
      if (el && !active) { el.disabled = true; const wrapper=el.closest('.field-item');if(wrapper)wrapper.hidden=true; }
    }
  }

  function installParameterTools() {
    const form = $(".input-card");
    const tools = document.createElement('div');
    tools.className = 'parameter-tools';
    tools.innerHTML = '<label>查找参数<input type="search" placeholder="输入名称、单位或关键词" aria-label="查找参数"></label><button type="button" data-fill-example>填入示例参数</button><p>示例仅用于试算。正式项目请逐项核定参数与资料来源。</p><label>本次参数资料来源<input type="text" data-source-note placeholder="例如：实测断面、图集页码或审定资料"></label>';
    (form.querySelector('.input-head') || form.querySelector('h2')).insertAdjacentElement('afterend', tools);
    tools.querySelector('[type="search"]').addEventListener('input', event => {
      const term = event.target.value.trim().toLowerCase();
      document.querySelectorAll('.field-item').forEach(item => item.hidden = !item.textContent.toLowerCase().includes(term));
      document.querySelectorAll('.param-group').forEach(group => group.hidden = !group.querySelector('.field-item:not([hidden])'));
    });
    tools.querySelector('[data-fill-example]').addEventListener('click', () => {
      invalidateResult();
      for (const field of config.fields) {
        const el = form.querySelector(`[name="${CSS.escape(field.key)}"]`);
        if (el && el.value.trim() === '' && field.default !== undefined) {
          el.value = field.default;
          state.sources[field.key] = '示例参数，待项目核定';
        }
      }
      setNotice('已填入空白项的示例值；请修改为项目实测或审定参数。', false);
      updateConditionalFields();
    });
    form.addEventListener('input', event => {
      if (event.target.type === 'search') return;
      invalidateResult();
      if (event.target.name) state.sources[event.target.name] = '人工录入或修订';
      updateConditionalFields();
    });
    form.addEventListener('change', event => {
      if (event.target.tagName === 'SELECT') {
        invalidateResult();
        state.sources[event.target.name] = '人工选择';
        updateConditionalFields();
      }
    });
    form.addEventListener('submit', event => { event.preventDefault(); calculate(); });
    invalidateResult();
    updateConditionalFields();
    if (config.programId === '4-02') {
      const link = document.createElement('a');
      link.className = 'back'; link.href = 'section-cad.html'; link.textContent = '从CAD导入断面';
      tools.appendChild(link);
      if (new URLSearchParams(location.search).get('cad') === 'reviewed') {
        try {
          const imported = JSON.parse(sessionStorage.getItem('htzd-reviewed-cad-section') || 'null');
          if (imported?.profile?.length >= 2) {
            form.querySelector('[name="points"]').value = imported.profile.map(p => `${p.offset},${p.elevation}`).join(';');
            state.sources.points = `CAD导入：${imported.file}；图层 ${imported.groundLayer || ''}；横距已以起点归零`;
            setNotice(`已带入 ${imported.profile.length} 个断面点。请核对图纸长度单位为米及高程基准，再补充糙率、比降和流量。`, false);
          }
        } catch (error) { setNotice('CAD断面记录无法读取，请重新导入。'); }
      }
    }
  }
  const rainFloodProfiles = new Set(["instantUnitLine", "rationalFormula"]);
  const chartPalette = ["#0a5382", "#1565c0", "#00838f", "#ef6c00", "#6a1b9a", "#2e7d32", "#4527a0", "#b45309", "#64748b", "#0f766e", "#be123c", "#2563eb"];
  const calcBookByProgram = {
    "1-01": "CB-01",
    "1-02": "CB-02",
    "1-03": "CB-02",
    "1-04": "CB-02",
    "1-05": "CB-02",
    "1-06": "CB-02",
    "1-08": "CB-02",
    "1-09": "CB-02",
    "1-10": "CB-02",
    "4-01": "CB-03",
    "4-02": "CB-03",
    "4-03": "CB-03",
    "5-01": "CB-04",
    "5-02": "CB-05",
    "5-03": "CB-05",
    "5-04": "CB-05",
    "5-05": "CB-05",
    "5-06": "CB-05",
    "5-07": "CB-05",
    "5-08": "CB-05",
    "5-09": "CB-05",
    "3-15": "CB-06",
    "3-16": "CB-07",
    "3-14": "CB-07",
    "3-13": "CB-08",
    "3-17": "CB-09",
    "3-12": "CB-10",
    "3-09": "CB-11",
    "3-10": "CB-11",
    "3-11": "CB-11",
    "2-01": "CB-12",
    "2-02": "CB-12",
    "2-03": "CB-12",
    "2-04": "CB-12",
    "2-05": "CB-12",
    "2-06": "CB-12",
    "2-07": "CB-12",
    "2-08": "CB-12",
    "2-09": "CB-12",
    "3-01": "CB-12",
    "3-02": "CB-12",
    "3-03": "CB-12",
    "3-04": "CB-12",
    "3-05": "CB-12",
    "3-06": "CB-12",
    "3-07": "CB-12",
    "3-08": "CB-12",
    "6-01": "CB-13",
    "6-02": "CB-13",
    "6-03": "CB-13",
    "6-04": "CB-13",
    "6-05": "CB-13",
    "6-06": "CB-13",
    "6-07": "CB-13",
    "7-01": "CB-14",
    "7-02": "CB-14",
    "7-03": "CB-14",
    "7-04": "CB-14",
    "7-05": "CB-14",
    "7-07": "CB-14",
    "7-08": "CB-14",
    "7-09": "CB-14",
  };

  function $(selector) {
    return document.querySelector(selector);
  }

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

  const labelOverrides = {
    area: "流域面积",
    area_km2: "流域面积",
    drainage_area_km2: "流域面积",
    design_flow_m3s: "设计流量",
    flow_m3s: "流量",
    design_water_level_m: "设计洪水位",
    roughness_n: "糙率 n",
    river_length_l1: "主河道长度 L₁",
    river_slope_j1: "主河道比降 J₁",
  };

  const unitOverrides = {
    area: "km²",
    area_km2: "km²",
    drainage_area_km2: "km²",
    design_flow_m3s: "m³/s",
    flow_m3s: "m³/s",
    design_water_level_m: "m",
    river_length_l1: "km",
    river_slope_j1: "‰",
  };

  function isBadLabel(value) {
    const text = String(value || "").trim();
    return !text || text.includes("?") || /^[A-Za-z0-9_\s./%-]+$/.test(text);
  }

  function prettifyKey(key) {
    return String(key || "").replace(/_/g, " ");
  }

  function cleanText(value) {
    return String(value || "")
      .replace(/\?+/g, " · ")
      .replace(/\s*·\s*$/g, "")
      .replace(/^\s*·\s*/g, "")
      .replace(/\s{2,}/g, " ")
      .trim();
  }

  function fieldLabel(field) {
    return isBadLabel(field.label) ? (labelOverrides[field.key] || prettifyKey(field.key)) : field.label;
  }

  function fieldUnit(field) {
    const raw = String(field.unit || "").trim();
    if (!raw || raw.includes("?")) return unitOverrides[field.key] || "";
    return raw;
  }

  const groupOrder = ["basic", "scenario", "geometry", "operating", "material", "rainfall", "loss", "routing", "reference", "structure", "control", "other"];
  const groupMeta = {
    scenario: {title:"计算方法与工况",summary:""},
    geometry: {title:"几何与测量资料",summary:""},
    operating: {title:"流量与运行条件",summary:""},
    material: {title:"材料与经验系数",summary:""},
    control: {title:"计算控制与输出范围",summary:""},
    basic: {
      title: "工程基础资料",
      summary: "录入控制断面、流域面积、河道长度、比降等工程基本资料；这些参数决定后续暴雨、产流、汇流和水位计算的适用范围。",
    },
    rainfall: {
      title: "设计暴雨与频率参数",
      summary: "录入点雨量均值、变差系数、偏态系数及面雨量折减系数。若后续接入等值线数据库，可由图集自动取值；当前仍以人工核定值为准。",
    },
    loss: {
      title: "产流、入渗与下垫面",
      summary: "录入土壤、植被、前期湿润程度、入渗分区、径流系数等参数，用于把面雨量转化为净雨过程。",
    },
    routing: {
      title: "汇流与过程控制",
      summary: "录入单位线分区、汇流参数、计算步长和过程线控制参数；用于形成净雨过程、单位线、洪水过程线或汇流曲线。",
    },
    reference: {
      title: "参证站与频率资料",
      summary: "录入参证站面积、流量、频率和回归点资料；用于水文面积比拟、双对数内插和频率适线等方法。",
    },
    structure: {
      title: "断面与建筑物参数",
      summary: "录入断面、水位、糙率、桥涵或水工建筑物参数；本组可直接用手动流量试算，不强制依赖上游洪水计算成果。",
    },
    other: {
      title: "专项计算参数",
      summary: "当前程序特有参数；请按资料来源逐项核对，不确定时先保留空值或采用经审定资料。",
    },
  };

  function fieldGroup(field) {
    const key = String(field.key || "").toLowerCase();
    const label = String(fieldLabel(field) || "");
    if (String(config.moduleKey) !== "1") {
      if (/name|region|zone/.test(key) || /名称|地区|分区/.test(label)) return "basic";
      if (/^method|^mode|_type$|^type|shape|formula/.test(key) || /方法|工况|类型|型式/.test(label)) return "scenario";
      if (/freq|return_period/.test(key) || /频率|重现期/.test(label)) return "reference";
      if (/step|count|iterations|tolerance|duration|time|n_points/.test(key) || /步长|次数|精度|时长|采样点数/.test(label)) return "control";
      if (/points|section|profile|station|length|width|height|depth|area|diameter|offset|radius|elevation|slope/.test(key) || /断面|长度|宽|高程|坡|厚|面积|直径/.test(label)) return "geometry";
      if (/roughness|coef|ratio|density|gravity|modulus|grain/.test(key) || /系数|糙率|密度|粒径|强度|配筋|模量/.test(label)) return "material";
      if (/flow|discharge|water|pressure|demand|power|head|speed|level/.test(key) || /流量|水位|压力|水头|功率|速度/.test(label)) return "operating";
      return "other";
    }
    if (key === "frequencies") return "basic";
    if (/rain|h1|h3|h6|h24|cv|cs|alpha|gamma|storm|precip|p\d+/.test(key) || /雨量|暴雨|面雨量|点雨量|变差|偏态|折减|雨型|形状修正/.test(label)) {
      return "rainfall";
    }
    if (/soil|moisture|infiltration|runoff|vegetation|loss|net|table_id/.test(key) || /土壤|湿润|入渗|径流|损失|下垫面|净雨/.test(label)) {
      return "loss";
    }
    if (/station|reference|sample|freq|frequency|return|n_value|regression|scale|area_1|area_2|q_ref/.test(key) || /参证|频率|比拟|回归|适线|面积比/.test(label)) {
      return "reference";
    }
    if (/unit_param|time_step|calc_step|step|duration|delta|dt_|_dt|routing|lag|tau|k_|j2|curve|hydrograph|(^|_)(m1|m2)(_|$)/.test(key) || /单位线|汇流|计算时段|步长|历时|过程线|洪峰/.test(label)) {
      return "routing";
    }
    if (/area|length|basin|river|name|region|zone|class|type/.test(key) || /流域|面积|长度|比降|名称|分区|类型|河道/.test(label)) {
      return "basic";
    }
    if (/section|water|level|roughness|bridge|pier|span|bed|foundation|scour|backwater|clearance|slope|flow_m3s|design_flow/.test(key) || /断面|水位|糙率|桥|墩|梁|床|基础|冲刷|壅水|净空|流量/.test(label)) {
      return "structure";
    }
    return "other";
  }

  function isRainFloodProfile() {
    return config && rainFloodProfiles.has(config.resultProfile);
  }

  function isFloodProgram() {
    return config && String(config.moduleKey || "").trim() === "1";
  }

  function getSelectedRegion() {
    if (!isFloodProgram()) return null;
    const code = typeof window.getSelectedHydrologyRegionCode === "function"
      ? window.getSelectedHydrologyRegionCode()
      : "GS";
    if (typeof window.getHydrologyRegionConfig === "function") return window.getHydrologyRegionConfig(code);
    return {
      code: "GS",
      name: "甘肃省",
      statusKind: "configured",
      statusLabel: "甘肃资料体系已启用",
      atlasSources: ["《甘肃省暴雨洪水图集》"],
      methodGroups: [],
      parameterGroups: [],
      notices: [],
      canCalculate: true,
    };
  }

  function isRegionBlocked() {
    const region = getSelectedRegion();
    return Boolean(isFloodProgram() && region && !region.canCalculate);
  }

  function renderFloodRegionContext() {
    document.querySelector(".region-context")?.remove();
    if (!isFloodProgram()) return;
    const hero = $(".input-head") || $(".hero");
    if (!hero || !hero.parentElement) return;
    const region = getSelectedRegion();
    if (!region) return;
    const atlasItems = (region.atlasSources || []).map((item) => `<li>${escapeHtml(item)}</li>`).join("");
    const noticeItems = (region.notices || []).map((item) => `<li>${escapeHtml(item)}</li>`).join("");
    const host = document.createElement("details");
    host.className = `region-context is-${region.canCalculate ? "configured" : "pending"}`;
    host.innerHTML = `
      <summary>
        <strong>${escapeHtml(region.name)}水文图集与参数库</strong>
        <span>${escapeHtml(region.statusLabel || "")}</span>
      </summary>
      <div class="region-context-grid">
        <section>
          <b>资料来源</b>
          <ul>${atlasItems || "<li>资料来源待接入</li>"}</ul>
        </section>
        <section>
          <b>计算约束</b>
          <ul>${noticeItems || "<li>按当前地区已接入公式和参数库计算。</li>"}</ul>
        </section>
      </div>
      ${region.canCalculate ? "" : `<a class="region-context-link" href="home-1.html">返回模块首页切换地区或补充本省图集资料</a>`}
    `;
    const select=document.createElement("select");
    select.setAttribute("aria-label","水文资料地区");
    for(const item of window.getHydrologyRegionOptions?.() || []){
      const option=document.createElement("option");option.value=item.code;option.textContent=item.name+(item.statusKind!=="configured"?"（资料未接入）":"");select.append(option);
    }
    select.value=region.code;
    const label=document.createElement("label");label.className="region-picker";label.textContent="水文资料地区";label.append(select);host.append(label);
    select.addEventListener("change",()=>{
      window.setSelectedHydrologyRegionCode(select.value);invalidateResult();renderFloodRegionContext();applyRegionGuardToButtons();
      document.querySelector(".input-card").dispatchEvent(new Event("change",{bubbles:true}));
    });
    hero.insertAdjacentElement("afterend", host);
  }

  function applyRegionGuardToButtons() {
    const blocked = isRegionBlocked();
    const primary = $(".primary");
    if (primary) primary.disabled = blocked;
    document.body.classList.toggle("is-region-blocked", blocked);
  }

  function renderRainFloodGuide() {
    if (!isRainFloodProfile()) return "";
    const methodTitle = config.resultProfile === "instantUnitLine" ? "瞬时单位线法" : "推理公式法";
    const thirdStep = config.resultProfile === "instantUnitLine"
      ? "用入渗扣损后的净雨过程卷积瞬时单位线，形成洪水过程线。"
      : "联立雨量式与汇流式求解 τ 和洪峰，再生成概化洪水过程线。";
    return `
      <details class="method-reference" aria-label="甘肃图集产汇流输入说明">
        <summary>参数选取依据与计算方法说明</summary>
        <div class="method-ref-head">
          <div>
            <strong>甘肃图集产汇流计算工作台 · ${escapeHtml(methodTitle)}</strong>
            <p>按参考计算器的工作流重组为“降雨输入 → 空间修正 → 产流扣损 → 汇流计算 → 成果图表”。所有参数必须来自图集、规范、实测或审定资料。</p>
          </div>
          <span class="method-status">等值线自动查算：待接入图集数据库</span>
        </div>
        <div class="rainfall-mode-tabs" role="tablist" aria-label="降雨输入方式">
          <button type="button" class="is-active" aria-selected="true">暴雨参数</button>
          <button type="button" disabled title="需接入已审定设计雨量数据库后启用">已知设计雨量</button>
          <button type="button" disabled title="需接入逐时段净雨资料后启用">经审定时段净雨</button>
        </div>
        <div class="method-steps">
          <article><b>1 降雨输入</b><span>H1、H6、H24 填点雨量均值；Cv、Cs/Cv 用于频率曲线计算。</span></article>
          <article><b>2 空间修正</b><span>面雨量 = 点雨量 × Kp × 点面折减 α × 形状修正 γ。</span></article>
          <article><b>3 产流扣损</b><span>入渗分区、Tc 和 f 控制面雨量转净雨；留空时按附表12自动查算。</span></article>
          <article><b>4 汇流计算</b><span>${escapeHtml(thirdStep)}</span></article>
        </div>
        <p class="method-warning">当前自动查等值线功能尚未接入矢量/栅格数据库，界面不会编造点雨量、Cv、α 或 γ；缺资料时请先补充图集读数或审定资料。</p>
      </details>
    `;
  }

  function fallbackHelp(field) {
    const key = String(field.key || "").toLowerCase();
    const label = fieldLabel(field);
    if (/area/.test(key) || /面积/.test(label)) return "请填控制断面以上汇水面积，单位按右侧标注；面积会影响汇流历时、面雨量折减和比拟换算。";
    if (/slope|比降/.test(key) || /比降/.test(label)) return "请确认是小数还是‰后再录入；界面单位为‰时直接填 95.724，单位为小数时填 0.095724。";
    if (/h\d|rain|雨量/.test(key) || /雨量/.test(label)) return "这里录入经图集、站点或审定资料确定的点雨量均值，不要把面雨量误填为点雨量。";
    if (/cv|变差/.test(key) || /变差/.test(label)) return "用于频率曲线计算的变差系数，应来自图集等值线、站点统计或审定资料。";
    if (/alpha|折减/.test(key) || /折减/.test(label)) return "点雨量换算面雨量的折减系数；缺少资料时不得随意编造。";
    if (/zone|region|分区/.test(key) || /分区/.test(label)) return "仅在该公式明确需要分区参数时选择；不需要分区的方法不会强制使用。";
    if (/station|参证/.test(key) || /参证/.test(label)) return "参证站应优先选择同一区域、下垫面和汇流条件相近的测站。";
    if (/flow|流量/.test(key) || /流量/.test(label)) return "可填上游设计洪水成果，也可录入外部审定流量用于水位试算。";
    if (/roughness|糙率/.test(key) || /糙率/.test(label)) return "糙率应按河床材料、植被、断面形态和规范表取值，并在计算书中说明依据。";
    return field.help || "请按设计资料、规范查表或已审定成果填写；来源不明确时先核对资料，不要随意取值。";
  }

  function renderField(field) {
    const label = escapeHtml(fieldLabel(field));
    const key = escapeHtml(field.key);
    const step = escapeHtml(field.step || "any");
    const safeUnit = fieldUnit(field);
    const unit = safeUnit ? `<small class="field-unit">${escapeHtml(safeUnit)}</small>` : "";
    const help = `<small class="field-help">${escapeHtml(field.help || fallbackHelp(field))}</small>`;
    if (field.type === "select") {
      const options = (field.options || []).map((option) => {
        const value = typeof option === "object" ? option.value : option;
        const text = typeof option === "object" ? option.label : option;
        const selected = value === field.default ? " selected" : "";
        return `<option value="${escapeHtml(value)}"${selected}>${escapeHtml(text)}</option>`;
      }).join("");
      return `<label class="field-item" data-field-key="${key}" for="${key}"><span>${label}</span><div class="field-control"><select id="${key}" name="${key}">${options}</select>${unit}</div>${help}</label>`;
    }
    const placeholder = field.default !== undefined && field.default !== ""
      ? `示例：${escapeHtml(field.default)}，请替换`
      : "请输入";
    const type = field.type || "number";
    if (type === 'textarea' || (type === 'text' && /points|sections|series|json|flows|inflows|demands/.test(field.key))) {
      return `<label class="field-item field-wide" data-field-key="${key}" for="${key}"><span>${label}${unit}</span><textarea id="${key}" name="${key}" rows="5" placeholder="${placeholder}"></textarea>${help}</label>`;
    }
    return `<label class="field-item" data-field-key="${key}" for="${key}"><span>${label}</span><div class="field-control"><input id="${key}" name="${key}" type="${type}" step="${step}" value="" placeholder="${placeholder}" />${unit}</div>${help}</label>`;
  }

  function renderFieldGroups(fields) {
    const buckets = new Map();
    for (const field of fields || []) {
      const key = fieldGroup(field);
      if (!buckets.has(key)) buckets.set(key, []);
      buckets.get(key).push(field);
    }
    return groupOrder
      .filter((key) => buckets.has(key))
      .map((key) => {
        const meta = groupMeta[key];
        const groupFields = buckets.get(key);
        return `
          <section class="param-group" data-group="${escapeHtml(key)}">
            <div class="param-group-header">
              <h3>${escapeHtml(meta.title)}</h3>
              <p>${escapeHtml(meta.summary)}</p>
            </div>
            <div class="group-grid">${groupFields.map(renderField).join("")}</div>
          </section>
        `;
      }).join("");
  }

  function updateConditionalFields() {
    const values = Object.fromEntries((config.fields || []).map((field) => {
      const element = document.querySelector(`[name="${CSS.escape(field.key)}"]`);
      return [field.key, element ? element.value : ""];
    }));
    for (const field of config.fields || []) {
      const wrapper = document.querySelector(`[data-field-key="${CSS.escape(field.key)}"]`);
      const element = document.querySelector(`[name="${CSS.escape(field.key)}"]`);
      if (!wrapper || !element) continue;
      const conditions = field.visibleWhen || {};
      const visible = Object.entries(conditions).every(([key, expected]) => {
        const allowed = Array.isArray(expected) ? expected : [expected];
        return allowed.includes(values[key]);
      });
      wrapper.hidden = !visible;
      element.disabled = !visible;
    }
    applyMethodConditions();
    document.querySelectorAll('.param-group').forEach((group) => {
      group.hidden = !Array.from(group.querySelectorAll('[data-field-key]')).some((field) => !field.hidden);
    });
  }

  function api() {
    return window.pywebview && window.pywebview.api;
  }

  function setNotice(message, isError = true) {
    const el = $(".notice");
    if (!el) return;
    el.style.color = isError ? "#dc2626" : "#0a5382";
    el.textContent = message || "";
  }

  function ensureDependencyHost() {
    let host = $(".dependency-warning");
    if (host) return host;
    const hero = $(".hero");
    if (!hero || !hero.parentElement) return null;
    host = document.createElement("div");
    host.className = "dependency-warning";
    hero.insertAdjacentElement("afterend", host);
    return host;
  }

  function renderDependencyWarning(payload) {
    const host = ensureDependencyHost();
    if (!host || !payload) return;
    state.dependency = payload;
    document.body.classList.remove("is-blocked");
    const primary = $(".primary");
    if (primary) primary.disabled = false;
    if (payload.ready !== false) {
      host.innerHTML = "";
      applyRegionGuardToButtons();
      return;
    }
    const required = (payload.required || [])
      .map((item) => `${escapeHtml(item.programId)} ${escapeHtml(item.name)}`)
      .join(" → ");
    const order = (payload.oneClickOrder || []).map(escapeHtml).join(" → ");
    host.innerHTML = `
      <div class="dependency-card">
        <strong>${escapeHtml(payload.message || "需先完成上游程序")}</strong>
        <span>缺失上游：${required || "未识别"}</span>
        <span>建议顺序：${order || escapeHtml(config.programId)}；也可手动录入本页参数试算。</span>
        <button type="button" class="dependency-action">打开上游程序</button>
      </div>
    `;
    const action = $(".dependency-action");
    if (action) action.addEventListener("click", openFirstMissingDependency);
    applyRegionGuardToButtons();
  }

  function openFirstMissingDependency() {
    const missing = (state.dependency && state.dependency.required) || [];
    if (!missing.length) return;
    const target = String(missing[0].programId || "").toLowerCase();
    if (!target) return;
    window.location.href = `${target}.html`;
  }

  async function checkDependencies() {
    if (!api() || typeof api().check_dependencies !== "function") return;
    try {
      const payload = await api().check_dependencies(config.programId);
      renderDependencyWarning(payload);
    } catch (error) {
      setNotice(`依赖检查失败：${error.message || error}`);
    }
  }

  function renderForm() {
    if (!config) {
      document.body.innerHTML = "<p>缺少页面配置。</p>";
      return;
    }
    const entry=window.WORKSPACE_ENTRIES?.find(p=>p.member_ids.includes(config.programId));
    document.title = entry?.name || config.title;
    $(".program-id").textContent = entry?.category || "";
    $(".program-title").textContent = entry?.name || config.title;
    $(".program-desc").textContent = cleanText(config.description || "");
    $(".program-source").textContent = cleanText(config.source || "");
    document.body.classList.toggle("is-rain-flood-profile", isRainFloodProfile());
    const inputTitle = $(".input-card h2");
    if (inputTitle) inputTitle.textContent = isRainFloodProfile() ? "输入参数与资料来源" : "输入参数";

    const form = $(".form-grid");
    document.querySelector(".method-reference")?.remove();
    const guide = renderRainFloodGuide();
    if (guide) form.insertAdjacentHTML("beforebegin", guide);
    form.innerHTML = renderFieldGroups(config.fields || []);
    form.addEventListener("change", updateConditionalFields);
    updateConditionalFields();
    renderFloodRegionContext();
    applyRegionGuardToButtons();
  }

  function collectParams() {
    const params = {};
    for (const field of config.fields) {
      const el = document.querySelector(`[name="${CSS.escape(field.key)}"]`);
      if (!el || el.disabled) continue;
      const raw = el.value.trim();
      if ((field.required !== false || (field.default !== undefined && field.default !== "")) && raw === "") {
        el.setAttribute('aria-invalid', 'true');
        el.closest('.field-item').hidden = false;
        el.closest('.param-group').hidden = false;
        el.focus();
        throw new Error(`请填写：${fieldLabel(field)}`);
      }
      el.removeAttribute('aria-invalid');
      if (field.type === "select" || field.kind === "text" || field.type === "text" || field.type === "textarea") {
        params[field.key] = raw;
      } else if (raw !== "") {
        const number = Number(raw);
        if (!Number.isFinite(number)) throw new Error(`${fieldLabel(field)} 必须是数字`);
        if (field.min !== undefined && number < field.min) throw new Error(`${fieldLabel(field)} 不能小于 ${field.min}`);
        if (field.max !== undefined && number > field.max) throw new Error(`${fieldLabel(field)} 不能大于 ${field.max}`);
        params[field.key] = number;
      }
    }
    if (isFloodProgram()) {
      const region = getSelectedRegion();
      params.hydrology_region_code = region.code;
      params.hydrology_region_name = region.name;
      params.hydrology_region_status = region.statusKind;
    }
    params._parameter_metadata = Object.fromEntries(config.fields.map(field => [field.key, {
      label: fieldLabel(field), unit: fieldUnit(field),
      source: `${state.sources[field.key] || (field.type === 'select' ? '表单预选，请复核' : '人工输入')}；${$('[data-source-note]')?.value.trim() || '资料来源待核定'}`,
    }]));
    return params;
  }

  async function calculate() {
    if (state.busy) return;
    invalidateResult();
    setNotice("");
    setResultActive(false);
    if (isRegionBlocked()) {
      const region = getSelectedRegion();
      setNotice(`${region.name}水文图集、公式和参数库尚未接入。请在本页水文资料地区中选择已接入地区，或补充并审定本省资料。`);
      return;
    }
    if (state.dependency && state.dependency.ready === false) {
      setNotice("上游成果缺失，本次将按本页手动参数试算；正式成果建议先完成上游。", false);
    }
    let params;
    try {
      params = collectParams();
    } catch (error) {
      setNotice(error.message || String(error));
      return;
    }
    if (!api() || typeof api().calculate !== "function") {
      setNotice("当前页面未连接 pywebview bridge，请从软件内打开。");
      return;
    }
    $(".result").innerHTML = '<div class="result-empty">正在计算...</div>';
    const revision = state.revision;
    state.busy = true;
    const button = document.querySelector('[data-action="calculate"]');
    button.disabled = true;
    button.textContent = '正在计算…';
    try {
    const payload = await api().calculate(config.programId, params);
    if (revision !== state.revision) {
      $(".result").innerHTML = '<div class="result-empty">计算期间参数已修改，请重新计算。</div>';
      return;
    }
    if (!payload || payload.ok === false) {
      $(".result").innerHTML = '<div class="result-empty">未生成结果。</div>';
      setNotice((payload && payload.message) || "计算失败。");
      return;
    }
    state.lastResult = payload.result;
    state.lastParams = params;
    setResultActive(true);
    renderResult(payload.result);
    $(".result").insertAdjacentHTML('afterbegin', `<details class="parameter-record"><summary>本次输入参数与来源 · ${(payload.result.parameter_snapshot || []).length}项</summary>${renderTable('本次输入参数与来源', payload.result.parameter_snapshot || [])}</details>`);
    if (payload.result.calculation_book_text) $(".result").insertAdjacentHTML('beforeend', `<details class="calculation-process"><summary>展开计算公式与完整过程</summary><pre>${escapeHtml(payload.result.calculation_book_text)}</pre></details>`);
    installFigureTools();
    document.querySelectorAll('[data-action="export"], [data-action="generate-book"], [data-action="export-cad-section"]').forEach(b => b.disabled = false);
    setNotice("计算完成。", false);
    window.dispatchEvent(new CustomEvent("calculation-complete", {detail: {programId:config.programId,params,result:payload.result}}));
    } catch (error) {
      $(".result").innerHTML = '<div class="result-empty">本次计算未完成。</div>';
      setNotice(`计算连接异常：${error.message || error}。可重试。`);
    } finally {
      state.busy = false;
      button.disabled = false;
      button.textContent = '开始计算';
      applyRegionGuardToButtons();
    }
  }

  function setResultActive(active) {
    const wrap = $(".result-card-wrap");
    if (wrap) wrap.classList.toggle("has-result", Boolean(active));
  }

  function renderResult(result) {
    if (isRainFloodProfile()) {
      renderRainFloodResult(result);
      return;
    }
    if (isWaterLevelProfile()) {
      renderWaterLevelResult(result);
      return;
    }
    const blocks = [];
    if (result.input_rows && result.input_rows.length) {
      blocks.push(renderTable("本次计算输入与单位", result.input_rows));
    }
    if (result.formula) {
      blocks.push(`<section class="result-card calculation-formula"><h3>采用公式与计算口径</h3><pre>${escapeHtml(result.formula)}</pre></section>`);
    }
    if (result.metrics) {
      blocks.push(renderTable("逐项中间量、结果与校核", result.metrics));
    }
    const contractPanel = renderContractPanel(result);
    if (contractPanel) blocks.push(contractPanel);
    const contractChart = renderContractSeriesChart(result);
    if (contractChart) blocks.push(contractChart);
    for (const table of result.tables || []) {
      blocks.push(renderTable(table.title, table.rows || [], table.columns || null, table.column_labels));
    }
    if (result.curve_points && result.curve_points.length) {
      blocks.push(renderCurve(result.curve_points));
    }
    if (result.rating_curve && result.rating_curve.length) {
      blocks.push(renderRatingCurveChart(result.chart_rating_curve || result.rating_curve, result.rating_markers || result.rating_marker || null, result.rating_chart_bounds));
    }
    if (result.profile_curve && result.profile_curve.length) {
      blocks.push(renderProfileCurveChart(result.profile_curve));
    }
    const sectionProfiles = result.section_profiles || (result.section_profile ? [result.section_profile] : []);
    if (sectionProfiles.length) {
      blocks.push(renderSectionProfiles(sectionProfiles));
    }
    for (const series of result.series || []) {
      blocks.push(renderSeries(series));
    }
    if (result.source) {
      blocks.push(`<p class="source">依据：${escapeHtml(result.source)}</p>`);
    }
    $(".result").innerHTML = blocks.join("") || '<div class="result-empty">无可显示结果。</div>';
  }

  function renderRainFloodResult(result) {
    const blocks = [];
    const metrics = normalizeRows(result.metrics || []);
    if (metrics.length) {
      blocks.push(renderKpiBoard(metrics, result));
    }
    const contractPanel = renderContractPanel(result);
    if (contractPanel) blocks.push(contractPanel);
    blocks.push(renderAtlasChartBoard(result));
    for (const table of result.tables || []) {
      blocks.push(renderTable(table.title, table.rows || [], table.columns || null, table.column_labels));
    }
    if (result.source) {
      blocks.push(`<p class="source">依据：${escapeHtml(result.source)}</p>`);
    }
    $(".result").innerHTML = blocks.join("") || '<div class="result-empty">无可显示结果。</div>';
  }

  function renderKpiBoard(rows, result) {
    const cards = rows.slice(0, 8).map((row) => `
      <article class="kpi-card">
        <span>${escapeHtml(row.name || row.item || row.label || "成果")}</span>
        <strong class="mono">${escapeHtml(displayValue(row.value ?? row.result ?? ""))}</strong>
        <small>${escapeHtml(row.unit || "")}</small>
        <p>${escapeHtml(row.note || row.description || "")}</p>
      </article>
    `).join("");
    const rainType = result.rain_type ? `<span class="result-chip">${escapeHtml(result.rain_type)}</span>` : "";
    return `
      <section class="result-card result-summary-card">
        <div class="result-toolbar">
          <div>
            <h3>计算成果总览</h3>
            <p class="result-note">先看洪峰、洪量、峰现时间等核心成果，再下钻查看过程线、参数表和计算书。</p>
          </div>
          ${rainType}
        </div>
        <div class="kpi-grid">${cards}</div>
      </section>
    `;
  }

  function normalizeDiag(value) {
    if (Array.isArray(value)) return value.map((item) => String(item || "").trim()).filter(Boolean);
    if (typeof value === "string" && value.trim()) return [value.trim()];
    return [];
  }

  function normalizeContractSeries(result) {
    const contract = result.calculation_contract || result.contract || {};
    const rows = Array.isArray(result.result_series)
      ? result.result_series
      : (Array.isArray(contract.result_series) ? contract.result_series : []);
    return rows.map((row, index) => {
      const source = row && typeof row === "object" ? row : {};
      const xRaw = source.time_h ?? source.time ?? source.t ?? source.x ?? index;
      const yRaw = source.value ?? source.flow_m3s ?? source.discharge_m3s ?? source.q ?? source.Q ?? source.y;
      const ciLowRaw = source.ci_low ?? source.low;
      const ciHighRaw = source.ci_high ?? source.high;
      return {
        x: finiteNumber(xRaw),
        y: finiteNumber(yRaw),
        ciLow: finiteNumber(ciLowRaw),
        ciHigh: finiteNumber(ciHighRaw),
      };
    }).filter((row) => Number.isFinite(row.x) && Number.isFinite(row.y));
  }

  function renderContractPanel(result) {
    const contract = result.calculation_contract || result.contract || {};
    const diag = normalizeDiag(result.diag || result.diagnostics || contract.diag);
    const status = result.status_code || contract.status_code || "";
    const mode = contract.mode || "design";
    const seriesCount = normalizeContractSeries(result).length;
    if (!diag.length && !status && !seriesCount) return "";
    const chips = [
      status ? `<span><strong>状态码</strong>${escapeHtml(status)}</span>` : "",
      mode ? `<span><strong>计算模式</strong>${escapeHtml(mode === "design" ? "设计计算" : mode)}</span>` : "",
      `<span><strong>成果序列</strong>${seriesCount ? `${seriesCount} 点` : "无逐时序列"}</span>`,
    ].filter(Boolean).join("");
    const diagItems = diag.map((item) => `<li>${escapeHtml(item)}</li>`).join("");
    return `
      <details class="result-card contract-card">
        <summary>计算状态与复核提示 · ${escapeHtml(status === 'ok' ? '已完成' : status)}</summary>
        <div class="result-toolbar">
          <div>
            <h3>计算状态与复核提示</h3>
            <p class="result-note">用于复核计算状态、过程线输出、图表完整性和计算书归档字段。</p>
          </div>
          <span class="result-chip">成果复核</span>
        </div>
        <div class="contract-grid">${chips}</div>
        ${diagItems ? `<ul class="diag-list">${diagItems}</ul>` : ""}
      </details>
    `;
  }

  function renderContractSeriesChart(result) {
    if ((result.series || []).length || result.rating_curve || result.profile_curve) return '';
    const points = normalizeContractSeries(result);
    if (!points.length) return "";
    const contract = result.calculation_contract || {};
    const lowArray = Array.isArray(result.ci_low) ? result.ci_low : (Array.isArray(contract.ci_low) ? contract.ci_low : []);
    const highArray = Array.isArray(result.ci_high) ? result.ci_high : (Array.isArray(contract.ci_high) ? contract.ci_high : []);
    const enriched = points.map((point, index) => {
      const ciLow = Number.isFinite(point.ciLow) ? point.ciLow : finiteNumber(lowArray[index]);
      const ciHigh = Number.isFinite(point.ciHigh) ? point.ciHigh : finiteNumber(highArray[index]);
      return { ...point, ciLow, ciHigh };
    });
    const yValues = enriched.flatMap((point) => {
      const values = [point.y];
      if (Number.isFinite(point.ciLow)) values.push(point.ciLow);
      if (Number.isFinite(point.ciHigh)) values.push(point.ciHigh);
      return values;
    });
    const width = 1080;
    const height = 390;
    const pad = { left: 68, right: 34, top: 42, bottom: 58 };
    const xExtent = chartExtent(enriched.map((point) => point.x), 0.04);
    const yExtent = chartExtent(yValues, 0.1);
    const chartId = `contract-${Math.random().toString(36).slice(2, 9)}`;
    const line = enriched.map((point) => {
      const p = projectSvg(point, xExtent, yExtent, width, height, pad);
      return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
    }).join(" ");
    const hasCi = enriched.some((point) => Number.isFinite(point.ciLow) && Number.isFinite(point.ciHigh));
    let band = "";
    if (hasCi) {
      const upper = enriched.map((point) => projectSvg({ x: point.x, y: Number.isFinite(point.ciHigh) ? point.ciHigh : point.y }, xExtent, yExtent, width, height, pad));
      const lower = enriched.slice().reverse().map((point) => projectSvg({ x: point.x, y: Number.isFinite(point.ciLow) ? point.ciLow : point.y }, xExtent, yExtent, width, height, pad));
      const polygon = [...upper, ...lower].map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ");
      band = `<polygon points="${polygon}" fill="#dbe7f5" opacity="0.72" />`;
    }
    return `
      <section class="result-card chart-card">
        <div class="chart-head">
          <div class="chart-title">
            <h3>统一成果序列${hasCi ? "与置信区间" : ""}</h3>
            <span>按命令书输出契约绘制；缺测值不补 0，不对曲线作平滑造假。</span>
          </div>
          <span class="chart-range">X ${fmtAxis(xExtent.rawMin)} - ${fmtAxis(xExtent.rawMax)} · Y ${fmtAxis(yExtent.rawMin)} - ${fmtAxis(yExtent.rawMax)}</span>
        </div>
        <svg class="chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="统一成果序列图">
          <defs>
            <linearGradient id="${chartId}-bg" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stop-color="#ffffff" />
              <stop offset="100%" stop-color="#f4f9fd" />
            </linearGradient>
          </defs>
          ${renderChartFrame({ width, height, pad, xExtent, yExtent, xLabel: "时间 t(h)", yLabel: "成果值", bgId: `${chartId}-bg` })}
          ${band}
          <polyline fill="none" stroke="#0a5382" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke" points="${line}" />
        </svg>
      </section>
    `;
  }

  function isWaterLevelProfile() {
    return ["4-01", "4-02", "4-03"].includes(String(config && config.programId));
  }

  function filterSeries(result, matcher) {
    return (result.series || []).filter((series) => matcher(String(series.title || "")));
  }

  function renderAtlasChartBoard(result) {
    const hydrograph = filterSeries(result, (title) => title.includes("洪水过程线"));
    const rainLoss = filterSeries(result, (title) => title.includes("面雨量过程") || title.includes("入渗量过程") || title.includes("净雨过程"));
    const unitLine = filterSeries(result, (title) => title.includes("单位线"));
    const confluence = filterSeries(result, (title) => title.includes("汇流曲线"));
    const groups = [
      {
        title: "设计洪水过程线",
        subtitle: "各计算频率同图对比，纵轴按成果自动缩放。",
        series: hydrograph,
        yLabel: "流量 m³/s",
      },
      {
        title: "面雨量、入渗量与净雨过程",
        subtitle: "用于核查点雨量换算、入渗扣损和净雨形成是否合理。",
        series: rainLoss,
        yLabel: "雨量 mm",
      },
    ];
    if (config.resultProfile === "instantUnitLine") {
      groups.push({
        title: "瞬时单位线 S(t)",
        subtitle: "按单位净雨深形成的汇流响应曲线，用于复核峰现时间和汇流形态。",
        series: unitLine,
        yLabel: "S(t)",
      });
    } else {
      groups.push({
        title: "推理公式汇流曲线",
        subtitle: "用于复核雨量式与汇流式交点、τ 和洪峰流量。",
        series: confluence,
        yLabel: "流量 m³/s",
      });
    }
    const charts = groups.map((group) => renderMultiSeriesChart(group)).filter(Boolean).join("");
    if (!charts) return "";
    return `
      <section class="atlas-chart-board">
        <div class="atlas-board-head">
          <h3>成果图表</h3>
          <p>过程线按软件真实计算点折线连接，不做平滑造假；导出计算书时同步保留成果表和图表数据。</p>
        </div>
        ${charts}
      </section>
    `;
  }

  function displayValue(value) {
    if (value === null || value === undefined) return "";
    if (typeof value === "number") {
      return Number.isInteger(value)
        ? String(value)
        : value.toLocaleString("zh-CN", { maximumSignificantDigits: 12, useGrouping: false });
    }
    return String(value);
  }

  function normalizeRows(rows) {
    if (!Array.isArray(rows)) {
      return Object.entries(rows || {}).map(([name, value]) => ({ name, value }));
    }
    return rows.map((row) => {
      if (Array.isArray(row)) {
        return { name: row[0], value: row[1], unit: row[2], note: row[3] };
      }
      return row || {};
    });
  }

  function renderTable(title, rows, columns = null, columnLabels = null) {
    const normalized = Array.isArray(columns) && columns.length && Array.isArray(rows) ? rows : normalizeRows(rows);
    const commonLabels = {name:'项目',value:'数值',unit:'单位',note:'说明',source:'资料来源',Fr:'弗汝德数 Fr'};
    const labels = Array.isArray(columnLabels) && columnLabels.length === columns?.length ? columnLabels : (columns || []).map(c => commonLabels[c] || c);
    if (Array.isArray(columns) && columns.length) {
      return `
        <section class="result-card">
          <div class="result-toolbar">
            <h3>${escapeHtml(title)}</h3>
            <span class="result-meta">${normalized.length} 行成果</span>
          </div>
          <div class="table-scroll engineering-table-wrap">
            <table class="engineering-table">
              <thead><tr>${labels.map((column) => `<th>${escapeHtml(column)}</th>`).join("")}</tr></thead>
              <tbody>
                ${normalized.map((row) => `
                  <tr>
                    ${columns.map((column, index) => {
                      const value = Array.isArray(row) ? row[index] : row[column];
                      const text = displayValue(value ?? "");
                      const mono = /^-?\d+(\.\d+)?$/.test(String(text).replace(/,/g, ""));
                      return `<td class="${mono ? "mono" : ""}">${escapeHtml(text)}</td>`;
                    }).join("")}
                  </tr>
                `).join("")}
              </tbody>
            </table>
          </div>
        </section>
      `;
    }
    return `
      <section class="result-card">
        <div class="result-toolbar">
          <h3>${escapeHtml(title)}</h3>
          <span class="result-meta">${normalized.length} 行成果</span>
        </div>
        <div class="table-scroll">
          <table>
            <thead><tr><th>项目</th><th>数值</th><th>单位</th><th>说明</th></tr></thead>
            <tbody>
              ${normalized.map((row) => `
                <tr>
                  <td>${escapeHtml(row.name || row.item || row.label || "")}</td>
                  <td class="mono">${escapeHtml(displayValue(row.value ?? row.result ?? ""))}</td>
                  <td>${escapeHtml(row.unit || "")}</td>
                  <td>${escapeHtml(row.note || row.description || "")}</td>
                </tr>
              `).join("")}
            </tbody>
          </table>
        </div>
      </section>
    `;
  }

  function chartExtent(values, paddingRatio = 0.06) {
    const clean = values.filter((value) => Number.isFinite(value));
    const min = Math.min(...clean);
    const max = Math.max(...clean);
    const span = Math.max(max - min, 0.001);
    const lower = min >= 0 ? Math.max(0, min - span * paddingRatio) : min - span * paddingRatio;
    const upper = max + span * paddingRatio;
    return {
      min: lower,
      max: upper,
      span: Math.max(upper - lower, 0.001),
      rawMin: min,
      rawMax: max,
    };
  }

  function chartTicks(extent, count = 4) {
    return Array.from({ length: count + 1 }, (_, index) => extent.min + (extent.span * index) / count);
  }

  function fmtAxis(value) {
    const abs = Math.abs(value);
    if (abs >= 1000) return value.toFixed(0);
    if (abs >= 100) return value.toFixed(1);
    return value.toFixed(2);
  }

  function renderChartFrame({ width, height, pad, xExtent, yExtent, xLabel, yLabel, bgId }) {
    const tickLabel = (tick, extent) => tick.toFixed(Math.max(0, Math.min(6, Math.ceil(-Math.log10(extent.span / 4)) + 1)));
    const plotW = width - pad.left - pad.right;
    const plotH = height - pad.top - pad.bottom;
    const xTicks = chartTicks(xExtent, 4);
    const yTicks = chartTicks(yExtent, 4);
    const vertical = xTicks.map((tick) => {
      const x = pad.left + ((tick - xExtent.min) / xExtent.span) * plotW;
      return `<line x1="${x.toFixed(1)}" y1="${pad.top}" x2="${x.toFixed(1)}" y2="${height - pad.bottom}" />`;
    }).join("");
    const horizontal = yTicks.map((tick) => {
      const y = height - pad.bottom - ((tick - yExtent.min) / yExtent.span) * plotH;
      return `<line x1="${pad.left}" y1="${y.toFixed(1)}" x2="${width - pad.right}" y2="${y.toFixed(1)}" />`;
    }).join("");
    const xLabels = xTicks.map((tick) => {
      const x = pad.left + ((tick - xExtent.min) / xExtent.span) * plotW;
      return `<text class="mono-text" x="${x.toFixed(1)}" y="${height - 15}" text-anchor="middle" fill="#5d7288" font-size="11">${tickLabel(tick, xExtent)}</text>`;
    }).join("");
    const yLabels = yTicks.map((tick) => {
      const y = height - pad.bottom - ((tick - yExtent.min) / yExtent.span) * plotH;
      return `<text class="mono-text" x="${pad.left - 10}" y="${(y + 4).toFixed(1)}" text-anchor="end" fill="#5d7288" font-size="11">${tickLabel(tick, yExtent)}</text>`;
    }).join("");
    return `
      <rect x="0" y="0" width="${width}" height="${height}" rx="16" fill="url(#${bgId})" />
      <g stroke="#d9e2ec" stroke-width="1" vector-effect="non-scaling-stroke">${vertical}${horizontal}</g>
      <line x1="${pad.left}" y1="${height - pad.bottom}" x2="${width - pad.right}" y2="${height - pad.bottom}" stroke="#829ab1" vector-effect="non-scaling-stroke" />
      <line x1="${pad.left}" y1="${pad.top}" x2="${pad.left}" y2="${height - pad.bottom}" stroke="#829ab1" vector-effect="non-scaling-stroke" />
      ${xLabels}${yLabels}
      <text x="${width - pad.right}" y="${height - 36}" text-anchor="end" fill="#486581" font-size="12">${escapeHtml(xLabel || "")}</text>
      <text x="${pad.left}" y="24" fill="#486581" font-size="12">${escapeHtml(yLabel || "")}</text>
    `;
  }

  function renderLineChart({ title, subtitle, ariaLabel, points, xLabel, yLabel, color = "#0a5382" }) {
    const cleanPoints = points.filter((point) => Number.isFinite(point.x) && Number.isFinite(point.y));
    if (!cleanPoints.length) return "";
    const width = 920;
    const height = 360;
    const pad = { left: 62, right: 30, top: 42, bottom: 54 };
    const xExtent = chartExtent(cleanPoints.map((point) => point.x), 0.02);
    const yExtent = chartExtent(cleanPoints.map((point) => point.y), 0.10);
    const plotW = width - pad.left - pad.right;
    const plotH = height - pad.top - pad.bottom;
    const project = (point) => ({
      x: pad.left + ((point.x - xExtent.min) / xExtent.span) * plotW,
      y: height - pad.bottom - ((point.y - yExtent.min) / yExtent.span) * plotH,
    });
    const poly = cleanPoints.map((point) => {
      const p = project(point);
      return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
    }).join(" ");
    const peak = cleanPoints.reduce((best, point) => (point.y > best.y ? point : best), cleanPoints[0]);
    const peakPx = project(peak);
    const markerStep = Math.max(1, Math.ceil(cleanPoints.length / 18));
    const chartId = `chart-${Math.random().toString(36).slice(2, 9)}`;
    const markers = cleanPoints
      .filter((_, index) => index % markerStep === 0)
      .map((point) => {
        const p = project(point);
        return `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3.2" fill="#fff" stroke="${color}" stroke-width="2" vector-effect="non-scaling-stroke"><title>${escapeHtml(xLabel)}: ${point.x}; ${escapeHtml(yLabel)}: ${point.y}</title></circle>`;
      }).join("");
    return `
      <section class="result-card chart-card">
        <div class="chart-head">
          <div class="chart-title">
            <h3>${escapeHtml(title)}</h3>
            <span>${escapeHtml(subtitle || "按软件计算结果绘制，坐标轴随成果自动缩放。")}</span>
          </div>
          <span class="chart-range">X ${fmtAxis(xExtent.rawMin)} - ${fmtAxis(xExtent.rawMax)} · Y ${fmtAxis(yExtent.rawMin)} - ${fmtAxis(yExtent.rawMax)}</span>
        </div>
        <svg class="chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeHtml(ariaLabel || title)}">
          <defs>
            <linearGradient id="${chartId}-bg" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stop-color="#ffffff" />
              <stop offset="100%" stop-color="#f4f9fd" />
            </linearGradient>
          </defs>
          ${renderChartFrame({ width, height, pad, xExtent, yExtent, xLabel, yLabel, bgId: `${chartId}-bg` })}
          ${segmentedLines(points, project, color, 3.2)}
          ${markers}
          <circle cx="${peakPx.x.toFixed(1)}" cy="${peakPx.y.toFixed(1)}" r="5.5" fill="#dc2626" stroke="#fff" stroke-width="2" vector-effect="non-scaling-stroke" />
          <text class="mono-text" x="${Math.min(width - 150, peakPx.x + 10).toFixed(1)}" y="${Math.max(32, peakPx.y - 10).toFixed(1)}" fill="#dc2626" font-size="12">峰值 ${displayValue(peak.y)}</text>
        </svg>
      </section>
    `;
  }

  function renderWaterLevelResult(result) {
    const blocks = [];
    const metrics = normalizeRows(result.metrics || []);
    if (metrics.length) {
      blocks.push(renderWaterKpiBoard(metrics, result));
    }
    const contractPanel = renderContractPanel(result);
    if (contractPanel) blocks.push(contractPanel);
    const chartBlocks = [];
    if (Array.isArray(result.rating_curve) && result.rating_curve.length) {
      chartBlocks.push(renderRatingCurveChart(result.chart_rating_curve || result.rating_curve, result.rating_markers || result.rating_marker || null, result.rating_chart_bounds));
    }
    if (Array.isArray(result.profile_curve) && result.profile_curve.length) {
      chartBlocks.push(renderProfileCurveChart(result.profile_curve));
    }
    const profiles = Array.isArray(result.section_profiles) && result.section_profiles.length
      ? result.section_profiles
      : (result.section_profile ? [result.section_profile] : []);
    if (profiles.length) {
      chartBlocks.push(renderSectionProfiles(profiles));
    }
    if (chartBlocks.length) {
      blocks.push(`
        <section class="water-chart-board">
          <div class="atlas-board-head">
            <div>
              <h3>水位成果图件</h3>
              <p>按计算成果自动绘制水位流量关系曲线、沿程水面线和断面示意图；所有标注均来自本次计算结果。</p>
            </div>
          </div>
          <div class="water-chart-grid">${chartBlocks.join("")}</div>
        </section>
      `);
    }
    for (const table of result.tables || []) {
      blocks.push(renderTable(table.title, table.rows || [], table.columns || null, table.column_labels));
    }
    if (result.source) {
      blocks.push(`<p class="source">依据：${escapeHtml(result.source)}</p>`);
    }
    $(".result").innerHTML = blocks.join("") || '<div class="result-empty">无可显示结果。</div>';
  }

  function renderWaterKpiBoard(rows, result) {
    const title = result.title || "水位计算成果";
    const cards = rows.slice(0, 6).map((row) => `
      <article class="water-kpi-card">
        <span>${escapeHtml(row.name || row.item || row.label || "成果")}</span>
        <strong class="mono">${escapeHtml(displayValue(row.value ?? row.result ?? ""))}</strong>
        <small>${escapeHtml(row.unit || "")}</small>
        <p>${escapeHtml(row.note || row.description || "")}</p>
      </article>
    `).join("");
    return `
      <section class="result-card water-summary-card">
        <div class="result-toolbar">
          <div>
            <h3>${escapeHtml(title)}</h3>
            <p class="result-note">先核对设计水位、流速、过水面积和能量线，再查看计算表与断面图。</p>
          </div>
          <span class="result-chip">水位 · 断面 · 曲线</span>
        </div>
        <div class="water-kpi-grid">${cards}</div>
      </section>
    `;
  }

  function projectSvg(point, xExtent, yExtent, width, height, pad) {
    const plotW = width - pad.left - pad.right;
    const plotH = height - pad.top - pad.bottom;
    return {
      x: pad.left + ((point.x - xExtent.min) / xExtent.span) * plotW,
      y: height - pad.bottom - ((point.y - yExtent.min) / yExtent.span) * plotH,
    };
  }

  function renderRatingCurveChart(points, marker, bounds) {
    const chartPoints = (points || []).map((p) => ({
      x: finiteNumber(p.discharge_m3s),
      y: finiteNumber(p.water_level_m),
    })).filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y)).sort((a, b) => a.y - b.y);
    if (!chartPoints.length) return "";
    const markers = (Array.isArray(marker) ? marker : marker ? [marker] : []).map((m) => ({
      x: finiteNumber(m.discharge_m3s), y: finiteNumber(m.water_level_m), label: String(m.label || '设计流量'),
    })).filter((m) => Number.isFinite(m.x) && Number.isFinite(m.y));
    const width = 920;
    const plotHeight = 390;
    const height = plotHeight + Math.ceil(markers.length / 2) * 44;
    const pad = { left: 82, right: 34, top: 42, bottom: 58 };
    const qMax = Math.max(...chartPoints.map(p => p.x), ...markers.map(m => m.x), 0.001) * 1.05;
    const xExtent = { min: 0, max: qMax, span: qMax };
    let minZ = Math.min(...chartPoints.map(p => p.y), ...markers.map(m => m.y));
    let maxZ = Math.max(...chartPoints.map(p => p.y), ...markers.map(m => m.y));
    if (Number.isFinite(finiteNumber(bounds?.min_water_level_m))) minZ = Math.min(minZ, Number(bounds.min_water_level_m));
    if (Number.isFinite(finiteNumber(bounds?.max_water_level_m))) maxZ = Math.max(maxZ, Number(bounds.max_water_level_m));
    if (maxZ <= minZ) maxZ = minZ + 0.01;
    const yExtent = { min: minZ, max: maxZ, span: maxZ - minZ };
    const chartId = `rating-${Math.random().toString(36).slice(2, 9)}`;
    const poly = chartPoints.map((point) => {
      const p = projectSvg(point, xExtent, yExtent, width, plotHeight, pad);
      return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
    }).join(" ");
    const colors = ['#c62828', '#00695c', '#7b1fa2', '#1565c0', '#ad5700', '#ad1457', '#455a64', '#558b2f'];
    // Keep every label in its own fixed row so close or coincident levels cannot overlap text.
    const markerSvg = markers.map((marker, index) => {
      const m = projectSvg(marker, xExtent, yExtent, width, plotHeight, pad);
      const color = colors[index % colors.length];
      const lx = 40 + (index % 2) * 440;
      const ly = plotHeight + Math.floor(index / 2) * 44 + 15;
      const label = `${index + 1}. ${marker.label}`;
      const detail = `Q=${marker.x.toFixed(3)} m³/s  Z=${marker.y.toFixed(3)} m`;
      return `<g class="rating-marker" data-label="${escapeHtml(marker.label)}">
        <title>${escapeHtml(`${label} ${detail}`)}</title>
        <line x1="${m.x.toFixed(1)}" y1="${m.y.toFixed(1)}" x2="${m.x.toFixed(1)}" y2="${plotHeight - pad.bottom}" stroke="${color}" stroke-dasharray="5 5" vector-effect="non-scaling-stroke" />
        <line x1="${pad.left}" y1="${m.y.toFixed(1)}" x2="${m.x.toFixed(1)}" y2="${m.y.toFixed(1)}" stroke="${color}" stroke-dasharray="5 5" vector-effect="non-scaling-stroke" />
        <circle cx="${m.x.toFixed(1)}" cy="${m.y.toFixed(1)}" r="5.5" fill="${color}" stroke="#fff" stroke-width="1.5" vector-effect="non-scaling-stroke" />
        <circle cx="${lx - 14}" cy="${ly - 4}" r="4" fill="${color}" />
        <text x="${lx}" y="${ly}" fill="${color}" font-size="13"${label.length > 44 ? ' textLength="390" lengthAdjust="spacingAndGlyphs"' : ''}>${escapeHtml(label)}</text>
        <text class="mono-text" x="${lx}" y="${ly + 17}" fill="#344054" font-size="12"${detail.length > 53 ? ' textLength="390" lengthAdjust="spacingAndGlyphs"' : ''}>${escapeHtml(detail)}</text>
      </g>`;
    }).join('');
    return `
      <article class="water-chart-card" data-manning-chart>
        <div class="chart-head">
          <div class="chart-title">
            <h3>水位流量关系曲线</h3>
          </div>
        </div>
        <svg class="chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="水位流量关系曲线">
          <defs>
            <linearGradient id="${chartId}-bg" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stop-color="#ffffff" />
              <stop offset="100%" stop-color="#f4f9fd" />
            </linearGradient>
          </defs>
          ${renderChartFrame({ width, height: plotHeight, pad, xExtent, yExtent, xLabel: "流量 Q(m³/s)", yLabel: "水位 Z(m)", bgId: `${chartId}-bg` })}
          <polyline fill="none" stroke="#0a5382" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke" points="${poly}" />
          ${markerSvg}
        </svg>
      </article>
    `;
  }

  function renderProfileCurveChart(points) {
    const clean = points.map((p) => ({
      x: finiteNumber(p.distance_m),
      water: finiteNumber(p.water_level_m),
      energy: finiteNumber(p.energy_grade_m),
      bed: finiteNumber(p.bed_elevation_m),
    })).filter((p) => Number.isFinite(p.x) && Number.isFinite(p.water));
    if (!clean.length) return "";
    const width = 720;
    const height = 390;
    const pad = { left: 70, right: 34, top: 42, bottom: 58 };
    const yValues = clean.flatMap((p) => [p.water, p.energy, p.bed].filter(Number.isFinite));
    const xExtent = chartExtent(clean.map((p) => p.x), 0.04);
    const yExtent = chartExtent(yValues, 0.12);
    const chartId = `profile-${Math.random().toString(36).slice(2, 9)}`;
    const lineFor = (key, fallbackKey) => clean
      .filter((p) => Number.isFinite(p[key]))
      .map((point) => {
        const p = projectSvg({ x: point.x, y: point[key] ?? point[fallbackKey] }, xExtent, yExtent, width, height, pad);
        return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
      }).join(" ");
    return `
      <article class="water-chart-card">
        <div class="chart-head">
          <div class="chart-title">
            <h3>沿程水面线与能量线</h3>
            <span>纵向对比河床最低高程、计算水位和能量线。</span>
          </div>
        </div>
        <svg class="chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="沿程水面线与能量线">
          <defs>
            <linearGradient id="${chartId}-bg" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stop-color="#ffffff" />
              <stop offset="100%" stop-color="#f4f9fd" />
            </linearGradient>
          </defs>
          ${renderChartFrame({ width, height, pad, xExtent, yExtent, xLabel: "距下游距离(m)", yLabel: "高程(m)", bgId: `${chartId}-bg` })}
          <polyline fill="none" stroke="#0a5382" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke" points="${lineFor("water")}" />
          <polyline fill="none" stroke="#f59e0b" stroke-width="2.4" stroke-dasharray="7 5" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke" points="${lineFor("energy")}" />
          <polyline fill="none" stroke="#64748b" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke" points="${lineFor("bed")}" />
        </svg>
        <div class="chart-legend">
          <span><i style="background:#0a5382"></i>计算水位</span>
          <span><i style="background:#f59e0b"></i>能量线</span>
          <span><i style="background:#64748b"></i>河床最低高程</span>
        </div>
      </article>
    `;
  }

  function renderSectionProfiles(profiles) {
    const items = profiles.map((profile) => renderSectionProfileChart(profile)).join("");
    return `<article class="water-chart-card water-section-gallery">${items}</article>`;
  }

  function renderSectionProfileChart(profile) {
    const points = (profile.points || []).map((p) => ({
      x: Number(p.x),
      z: Number(p.z),
    })).filter((p) => Number.isFinite(p.x) && Number.isFinite(p.z));
    if (points.length < 2) return "";
    const waterLevel = finiteNumber(profile.water_level);
    const thalwegX = finiteNumber(profile.thalweg_x);
    const thalwegZ = finiteNumber(profile.thalweg_z);
    const width = 640;
    const height = 330;
    const pad = { left: 54, right: 28, top: 34, bottom: 46 };
    const xExtent = chartExtent(points.map((p) => p.x), 0.04);
    const zValues = points.map((p) => p.z);
    if (Number.isFinite(waterLevel)) zValues.push(waterLevel);
    const yExtent = chartExtent(zValues, 0.16);
    const project = (point) => projectSvg({ x: point.x, y: point.z }, xExtent, yExtent, width, height, pad);
    const ground = points.map((point) => {
      const p = project(point);
      return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
    }).join(" ");
    const wlY = Number.isFinite(waterLevel)
      ? projectSvg({ x: points[0].x, y: waterLevel }, xExtent, yExtent, width, height, pad).y
      : null;
    const minX = pad.left;
    const maxX = width - pad.right;
    const minPoint = points.reduce((best, item) => (item.z < best.z ? item : best), points[0]);
    const thalwegPoint = Number.isFinite(thalwegX) && Number.isFinite(thalwegZ)
      ? { x: thalwegX, z: thalwegZ }
      : minPoint;
    const thalweg = project(thalwegPoint);
    const chartId = `section-${Math.random().toString(36).slice(2, 9)}`;
    return `
      <div class="section-profile-card">
        <div class="section-profile-title">
          <strong>${escapeHtml(profile.name || "断面图")}</strong>
          <span>设计水位、深泓点与断面线</span>
        </div>
        <svg class="section-chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeHtml(profile.name || "断面图")}">
          <defs>
            <linearGradient id="${chartId}-bg" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stop-color="#ffffff" />
              <stop offset="100%" stop-color="#f4f9fd" />
            </linearGradient>
          </defs>
          ${renderChartFrame({ width, height, pad, xExtent, yExtent, xLabel: "起点距(m)", yLabel: "高程(m)", bgId: `${chartId}-bg` })}
          ${wlY !== null ? `<rect x="${minX}" y="${wlY.toFixed(1)}" width="${(maxX - minX).toFixed(1)}" height="${(height - pad.bottom - wlY).toFixed(1)}" fill="rgba(80,163,173,0.15)" />` : ""}
          ${wlY !== null ? `<line x1="${minX}" y1="${wlY.toFixed(1)}" x2="${maxX}" y2="${wlY.toFixed(1)}" stroke="#1565c0" stroke-width="2.4" stroke-dasharray="7 4" vector-effect="non-scaling-stroke" />` : ""}
          <polyline fill="none" stroke="#102a43" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" vector-effect="non-scaling-stroke" points="${ground}" />
          <circle cx="${thalweg.x.toFixed(1)}" cy="${thalweg.y.toFixed(1)}" r="5" fill="#dc2626" stroke="#fff" stroke-width="2" vector-effect="non-scaling-stroke" />
          ${wlY !== null ? `<text class="mono-text" x="${(minX + 12).toFixed(1)}" y="${(wlY - 8).toFixed(1)}" fill="#1565c0" font-size="12">设计水位 Z=${waterLevel.toFixed(2)} m</text>` : ""}
          <text class="mono-text" x="${Math.min(width - 190, thalweg.x + 10).toFixed(1)}" y="${Math.max(26, thalweg.y - 10).toFixed(1)}" fill="#dc2626" font-size="12">深泓点 ${thalwegPoint.z.toFixed(2)} m</text>
        </svg>
      </div>
    `;
  }

  function seriesToPoints(series) {
    return (series.points || [])
      .map((p) => ({
        x: finiteNumber(p[series.xKey || "time_h"]),
        y: finiteNumber(p[series.yKey || "discharge_m3s"]),
      }));
  }

  function renderMultiSeriesChart({ title, subtitle, series, yLabel }) {
    const prepared = (series || [])
      .map((item, index) => ({
        title: item.title || `曲线 ${index + 1}`,
        points: seriesToPoints(item),
        color: item.color || chartPalette[index % chartPalette.length],
        xLabel: item.xLabel || item.xKey || "time_h",
        yLabel: item.yLabel || item.unit || yLabel || "",
      }))
      .filter((item) => item.points.length);
    if (!prepared.length) return "";
    const width = 1080;
    const height = 420;
    const pad = { left: 66, right: 36, top: 46, bottom: 58 };
    const allPoints = prepared.flatMap((item) => item.points).filter(p => Number.isFinite(p.x) && Number.isFinite(p.y));
    if (!allPoints.length) return '';
    const xExtent = chartExtent(allPoints.map((point) => point.x), 0.02);
    const yExtent = chartExtent(allPoints.map((point) => point.y), 0.10);
    const plotW = width - pad.left - pad.right;
    const plotH = height - pad.top - pad.bottom;
    const project = (point) => ({
      x: pad.left + ((point.x - xExtent.min) / xExtent.span) * plotW,
      y: height - pad.bottom - ((point.y - yExtent.min) / yExtent.span) * plotH,
    });
    const chartId = `chart-${Math.random().toString(36).slice(2, 9)}`;
    const polylines = prepared.map((item) => {
      const poly = item.points.map((point) => {
        const p = project(point);
        return `${p.x.toFixed(1)},${p.y.toFixed(1)}`;
      }).join(" ");
      const valid = item.points.filter(p => Number.isFinite(p.x) && Number.isFinite(p.y));
      if (!valid.length) return '';
      const peak = valid.reduce((best, point) => (point.y > best.y ? point : best), valid[0]);
      const peakPx = project(peak);
      return `
        ${segmentedLines(item.points, project, item.color)}
        <circle cx="${peakPx.x.toFixed(1)}" cy="${peakPx.y.toFixed(1)}" r="3.6" fill="${item.color}" stroke="#fff" stroke-width="1.5" vector-effect="non-scaling-stroke" />
      `;
    }).join("");
    const legend = prepared.map((item) => `
      <span><i style="background:${escapeHtml(item.color)}"></i>${escapeHtml(item.title)}</span>
    `).join("");
    return `
      <section class="result-card chart-card atlas-chart-card">
        <div class="chart-head">
          <div class="chart-title">
            <h3>${escapeHtml(title)}</h3>
            <span>${escapeHtml(subtitle || "按软件计算成果绘制。")}</span>
          </div>
          <span class="chart-range">X ${fmtAxis(xExtent.rawMin)} - ${fmtAxis(xExtent.rawMax)} · Y ${fmtAxis(yExtent.rawMin)} - ${fmtAxis(yExtent.rawMax)}</span>
        </div>
        <svg class="chart" viewBox="0 0 ${width} ${height}" role="img" aria-label="${escapeHtml(title)}">
          <defs>
            <linearGradient id="${chartId}-bg" x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stop-color="#ffffff" />
              <stop offset="100%" stop-color="#f4f9fd" />
            </linearGradient>
          </defs>
          ${renderChartFrame({ width, height, pad, xExtent, yExtent, xLabel: prepared[0].xLabel, yLabel: yLabel || prepared[0].yLabel, bgId: `${chartId}-bg` })}
          ${polylines}
        </svg>
        <div class="chart-legend">${legend}</div>
      </section>
    `;
  }

  function renderCurve(points) {
    const chartPoints = (points || [])
      .map((p) => ({
        x: Number(p.distance_m ?? p.x ?? p.station_m ?? 0),
        y: Number(p.water_level_m ?? p.backwater_m ?? p.y),
      }))
      .filter((p) => Number.isFinite(p.x) && Number.isFinite(p.y));
    return renderLineChart({
      title: "沿程曲线图",
      subtitle: "用于查看壅水、水位或水面线沿程变化。",
      ariaLabel: "沿程曲线",
      points: chartPoints,
      xLabel: "距离 m",
      yLabel: "水位 / 壅水 m",
      color: "#0a5382",
    });
  }

  function renderSeries(series) {
    const points = seriesToPoints(series);
    return renderLineChart({
      title: series.title || "过程曲线",
      subtitle: "过程线按真实成果点折线连接，不做平滑造假。",
      ariaLabel: series.title || "过程曲线",
      points,
      xLabel: series.xLabel || series.xKey || "time_h",
      yLabel: series.yLabel || series.unit || "",
      color: series.color || "#1565c0",
    });
  }

  function showFormDialog({ title, description, defaultPath, confirmText }) {
    return new Promise((resolve) => {
      document.querySelector(".form-dialog-backdrop")?.remove();
      const backdrop = document.createElement("div");
      backdrop.className = "form-dialog-backdrop";
      backdrop.innerHTML = `
        <form class="form-dialog-card" role="dialog" aria-modal="true" aria-label="${escapeHtml(title)}">
          <h3>${escapeHtml(title)}</h3>
          <p>${escapeHtml(description)}</p>
          <div class="dialog-grid">
            <label class="dialog-field">
              <span>导出路径</span>
              <input name="target_path" type="text" value="${escapeHtml(defaultPath)}" required>
              <small>可填写 .docx 或 .xlsx 路径；建议保存到 D:\\workspace 或项目成果目录。</small>
            </label>
            <label class="dialog-field">
              <span>稿件类型</span>
              <select name="watermark">
                <option value="送审稿">送审稿</option>
                <option value="报批稿">报批稿</option>
                <option value="正式稿">正式稿</option>
              </select>
              <small>该选项会用于计算书水印和成果标识。</small>
            </label>
          </div>
          <div class="dialog-actions">
            <button type="button" class="secondary" data-dialog-cancel>取消</button>
            <button type="submit" class="primary">${escapeHtml(confirmText || "导出")}</button>
          </div>
        </form>
      `;
      const close = (value) => {
        backdrop.remove();
        resolve(value);
      };
      backdrop.addEventListener("click", (event) => {
        if (event.target === backdrop) close(null);
      });
      backdrop.querySelector("[data-dialog-cancel]")?.addEventListener("click", () => close(null));
      backdrop.querySelector("form")?.addEventListener("submit", (event) => {
        event.preventDefault();
        const formData = new FormData(event.currentTarget);
        const target = String(formData.get("target_path") || "").trim();
        const watermark = String(formData.get("watermark") || "送审稿").trim();
        if (!target) return;
        close({ target, watermark });
      });
      document.body.appendChild(backdrop);
      backdrop.querySelector("input")?.focus();
    });
  }

  async function exportBook(format = 'docx') {
    if (typeof format !== 'string') format = 'docx';
    if (!state.lastResult) {
      setNotice("请先计算，再导出计算书。");
      return;
    }
    if (!api() || typeof api().export_calc_book !== "function") {
      setNotice("当前页面未连接导出 bridge。");
      return;
    }
    const defaultName = `${config.programId}_${config.title}.${format}`.replace(/[\\/:*?"<>|]/g, "_");
    const options = await showFormDialog({
      title: "导出计算书",
      description: "导出当前计算成果、参数表和过程说明。导出前请确认已经完成本页计算。",
      defaultPath: `D:\\workspace\\${defaultName}`,
      confirmText: "开始导出",
    });
    if (!options) return;
    const payload = await api().export_calc_book(config.programId, {
      ...state.lastResult,
      target_path: options.target,
      watermark: options.watermark,
      calculation_book_text: state.lastResult.calculation_book_text || "",
    });
    if (!payload || payload.ok === false) {
      setNotice((payload && payload.message) || "导出失败。");
      return;
    }
    setNotice(`已导出：${payload.path}`, false);
  }

  function canExportCadSection() {
    if (!config) return false;
    if (["4-01", "4-02", "4-03"].includes(String(config.programId))) return true;
    const result = state.lastResult || {};
    return Boolean(result.section_profile || (Array.isArray(result.section_profiles) && result.section_profiles.length));
  }

  function installCadExportButton() {
    const actions = $(".actions");
    if (!actions || !canExportCadSection()) return;
    if (document.querySelector('[data-action="export-cad-section"]')) return;
    const button = document.createElement("button");
    button.type = "button";
    button.className = "export-cad-section";
    button.dataset.action = "export-cad-section";
    button.textContent = "导出CAD断面";
    button.title = "导出DXF断面图，自动标注设计水位和深泓点。";
    actions.appendChild(button);
  }

  function showCadExportDialog({ defaultPath }) {
    return new Promise((resolve) => {
      document.querySelector(".form-dialog-backdrop")?.remove();
      const backdrop = document.createElement("div");
      backdrop.className = "form-dialog-backdrop";
      backdrop.innerHTML = `
        <form class="form-dialog-card" role="dialog" aria-modal="true" aria-label="导出CAD断面">
          <h3>导出CAD断面</h3>
          <p>导出当前计算断面为 DXF 文件，图内自动标注地面线、设计水位线和深泓点。</p>
          <div class="dialog-grid">
            <label class="dialog-field">
              <span>导出路径</span>
              <input name="target_path" type="text" value="${escapeHtml(defaultPath)}" required>
              <small>仅导出 .dxf 文件，可直接用 AutoCAD、天正或常用 CAD 软件打开。</small>
            </label>
          </div>
          <div class="dialog-actions">
            <button type="button" class="secondary" data-dialog-cancel>取消</button>
            <button type="submit" class="primary">导出DXF</button>
          </div>
        </form>
      `;
      const close = (value) => {
        backdrop.remove();
        resolve(value);
      };
      backdrop.addEventListener("click", (event) => {
        if (event.target === backdrop) close(null);
      });
      backdrop.querySelector("[data-dialog-cancel]")?.addEventListener("click", () => close(null));
      backdrop.querySelector("form")?.addEventListener("submit", (event) => {
        event.preventDefault();
        const formData = new FormData(event.currentTarget);
        const target = String(formData.get("target_path") || "").trim();
        if (!target) return;
        close({ target });
      });
      document.body.appendChild(backdrop);
      backdrop.querySelector("input")?.focus();
    });
  }

  async function exportCadSection() {
    if (!state.lastResult) {
      setNotice("请先完成水位或断面计算，再导出CAD断面。");
      return;
    }
    if (!api() || typeof api().export_section_cad !== "function") {
      setNotice("当前页面未连接CAD断面导出接口。");
      return;
    }
    const defaultName = `${config.programId}_${config.title}_断面水位图.dxf`.replace(/[\\/:*?"<>|]/g, "_");
    const options = await showCadExportDialog({
      defaultPath: `D:\\workspace\\cad_exports\\${defaultName}`,
    });
    if (!options) return;
    const payload = await api().export_section_cad(config.programId, {
      target_path: options.target,
      result: state.lastResult,
      params: state.lastParams || {},
    });
    if (!payload || payload.ok === false) {
      setNotice((payload && payload.message) || "CAD断面导出失败。");
      return;
    }
    setNotice(`已导出 ${payload.section_count || 1} 个CAD断面：${payload.path}；其余断面位于同一目录。`, false);
  }

  function installCalcBookButton() {
    const current = document.querySelector('[data-action="generate-book"]');
    if (current) {
      current.textContent = '导出Excel图表';
      current.title = '导出本次计算的参数、过程、成果表及完整坐标曲线';
      current.disabled = true;
    }
  }

  async function generateStructuredCalcBook() {
    return exportBook('xlsx');
  }

  window.CalculationEngine = {
    draft() {
      return {
        fields: Object.fromEntries((config.fields || []).map(field => [field.key, document.querySelector('[name="' + CSS.escape(field.key) + '"]')?.value ?? ""])),
        sources: {...state.sources}, sourceNote: document.querySelector("[data-source-note]")?.value || "",
        regionCode: isFloodProgram() ? window.getSelectedHydrologyRegionCode?.() : null
      };
    },
    restore(draft) {
      invalidateResult();
      for (const [key,value] of Object.entries(draft?.fields || {})) {
        const field = document.querySelector('[name="' + CSS.escape(key) + '"]');
        if(field)field.value = value ?? "";
      }
      state.sources = {...(draft?.sources || {})};
      const note = document.querySelector("[data-source-note]");
      if(note)note.value = draft?.sourceNote || "";
      if(draft?.regionCode && isFloodProgram())window.setSelectedHydrologyRegionCode?.(draft.regionCode);
      renderFloodRegionContext();applyRegionGuardToButtons();
      updateConditionalFields();
    },
    restoreResult(params, result) {
      this.restore({fields:params,regionCode:params.hydrology_region_code});
      state.lastParams = params; state.lastResult = result;
      renderResult(result);
      if (result.calculation_book_text) $(".result").insertAdjacentHTML("beforeend", '<details class="calculation-process"><summary>计算公式与完整过程</summary><pre>' + escapeHtml(result.calculation_book_text) + '</pre></details>');
      installFigureTools(); setResultActive(true);
      document.querySelectorAll('[data-action="export"],[data-action="generate-book"],[data-action="export-cad-section"]').forEach(b => b.disabled=false);
      setNotice("已打开历史成果快照。",false);
    },
    fields: () => config.fields,
    calculate, exportBook, exportCadSection
  };

  document.addEventListener("DOMContentLoaded", async () => {
    await loadCanonicalInputSchema();
    renderForm();
    installCalcBookButton();
    installCadExportButton();
    installParameterTools();
    checkDependencies();
    const calculateButton = document.querySelector('[data-action="calculate"]');
    const exportButton = document.querySelector('[data-action="export"]');
    const generateButton = document.querySelector('[data-action="generate-book"]');
    const cadExportButton = document.querySelector('[data-action="export-cad-section"]');
    if (calculateButton) calculateButton.addEventListener("click", calculate);
    if (exportButton) exportButton.addEventListener("click", exportBook);
    if (generateButton) generateButton.addEventListener("click", generateStructuredCalcBook);
    if (cadExportButton) cadExportButton.addEventListener("click", exportCadSection);
    const back = $(".back");
    if (back && !back.getAttribute("href")) {
      back.addEventListener("click", () => {
        window.location.href = "../index.html";
      });
    }
    window.dispatchEvent(new CustomEvent("calculation-ready", {detail:{programId:config.programId}}));
  });
})();
