import type { QualityComponentDefinition, QualityProps } from '../core/types';

import { QUALITY_REPEAT_ATTR, QUALITY_REPEAT_ROW_ATTR } from '../core/attrs';
import { parseColumnSpec, parseHeaderSpans } from '../core/column-spec';
import { cssStyle, escapeHtml } from '../core/html';
import { QUALITY_ICONS } from '../core/icons';
import { QUALITY_CATEGORY } from '../core/types';

/** 检验项数组在报告上下文中的默认路径 */
const DEFAULT_ITEMS_PATH = 'inspectionItems';

/** 循环变量名，行内占位符写作 {{item.xxx}} */
const LOOP_VAR = 'item';

/** 渲染期由重复容器注入的行号变量，写作 {{index}}，从 1 开始 */
const INDEX_VAR = 'index';

const CELL_BORDER = '1px solid #d9d9d9';

/** 单元格静态样式（合并格与普通格一致，差别只在属性） */
const CELL_STYLE = cssStyle({ border: CELL_BORDER, padding: '6px 8px' });

/**
 * 自定列时的单元格样式，比默认列多一条允许长串中途折行。
 * <p>
 * 默认列带手工调过的宽度（序号 48px、检测要求 30%、实测值 26%……），宽度由这些声明说了算；
 * 自定列一个宽度都没有，整张表的列宽就只能由各自的**最小内容宽度**倒推。而报告里的值是
 * `{{item.standardValue}}` 这种不含空格的长串，最小内容宽度就是整串的长度——几列加起来
 * 轻易超过 A4 正文宽度，浏览器只能把表格撑出纸张，最末一列被挤成十几个像素的竖条。
 * <p>
 * `anywhere` 会参与最小内容宽度的计算（`break-word` 不会），列宽因此不再被长串绑架：
 * 值在**出件时**本来就短（「烘箱干燥法」「13.2」），折行极少触发，产物与设计器所见一致。
 * <p>
 * 只在自定列时加：默认列的宽度声明已经定住了版面，再叠一条折行反而让「检验项目」「子项」
 * 这类没写宽度的列被挤到 30 余像素、行高翻几倍——实测过，不加比加好。
 */
const CUSTOM_CELL_STYLE = cssStyle({
  border: CELL_BORDER,
  padding: '6px 8px',
  'overflow-wrap': 'anywhere',
});

/**
 * 组名格的合并属性。
 * <p>
 * 只能写成「属性」而不能写进行内 style：行内样式会被 GrapesJS 解析成节点 style 对象，
 * 而渲染期对 style 对象不跑绑定（renderStyle），绑定串会被整条丢掉；
 * 属性则原样保留，两侧渲染器都会把 {{}} 解成值。
 * <p>
 * hidden 用字符串而非布尔：这个属性靠「在不在」起作用（hidden="false" 照样隐藏），
 * 所以只有「空串 = 不输出该属性 = 可见」这一个可判定的形态。
 * 两侧渲染器都跳过求值后为空串的属性（render.ts / HtmlRenderer.java），行为逐字一致。
 */
const GROUP_CELL_ATTRS =
  ` rowspan="{{${LOOP_VAR}.group.span}}" hidden="{{${LOOP_VAR}.group.hidden}}"`;

interface ColumnSpec {
  /** 表头文字 */
  label: string;
  /** 行内取值表达式 */
  expr: string;
  /** 写在 <td> 上的额外属性（绑定） */
  attrs?: string;
  /** 对应的显示开关属性名，空串表示不可隐藏 */
  toggle: string;
  width?: string;
}

const COLUMNS: ColumnSpec[] = [
  { label: '序号', expr: `{{${INDEX_VAR}}}`, toggle: 'showIndex', width: '48px' },
  { label: '检验项目', expr: `{{${LOOP_VAR}.itemName}}`, attrs: GROUP_CELL_ATTRS, toggle: '' },
  { label: '子项', expr: `{{${LOOP_VAR}.group.childName}}`, toggle: '' },
  {
    label: '检测要求',
    expr: `{{${LOOP_VAR}.requirement}}`,
    toggle: 'showRequirement',
    width: '30%',
  },
  {
    label: '实测值',
    expr: `{{${LOOP_VAR}.actualValue}}`,
    toggle: 'showActual',
    width: '26%',
  },
  { label: '单位', expr: `{{${LOOP_VAR}.unit}}`, toggle: 'showUnit', width: '60px' },
  { label: '判定', expr: `{{${LOOP_VAR}.resultText}}`, toggle: 'showResult', width: '72px' },
];

/** 真正参与出表的一列：默认列与自定义列都收敛成这个形状再拼 HTML */
interface RenderColumn {
  label: string;
  expr: string;
  /** 是不是纵向合并跨住整组的组名列 */
  merge: boolean;
  width?: string;
}

/** 按显示开关挑出默认列 */
function defaultColumns(properties: QualityProps): RenderColumn[] {
  return COLUMNS.filter(
    (column) => column.toggle === '' || properties[column.toggle] !== false,
  ).map((column) => ({
    label: column.label,
    expr: column.expr,
    merge: Boolean(column.attrs),
    width: column.width,
  }));
}

/**
 * 出表用的列：填了 columns 就整表照它来，否则用默认列。
 * <p>
 * 自定义列时**不再看各显示开关**：columns 已经把列与顺序说全了，
 * 再叠一层「序号被隐藏了但用户又写回来了」的开关判断，只会让两边打架。
 *
 * @return custom 为真表示这张表照 columns 来（拿不到任何宽度声明，单元格要允许折行）
 */
function effectiveColumns(properties: QualityProps): {
  columns: RenderColumn[];
  custom: boolean;
} {
  const custom = parseColumnSpec(textOf(properties.columns));
  return custom.items.length > 0
    ? { columns: custom.items, custom: true }
    : { columns: defaultColumns(properties), custom: false };
}

function textOf(value: unknown): string {
  return String(value ?? '').trim();
}

/** 上层的出表方案：上层那一行的单元格，以及下层要跳过哪些列 */
interface HeaderPlan {
  cells: string;
  covered: Set<number>;
}

/**
 * 规划上层表头。
 * <p>
 * 表头里的格子有两种形态，都从同一串 `标题^跨列数` 里认出来：
 * <ul>
 *   <li>分组标题：横向跨住下面若干列，如 `检验结果^5`；</li>
 *   <li>纵向合并格：某一列自己既是列名又是上格，原件里是一格跨住上下两层
 *       （提取文本里表现为下层表头那一格写着「↑同上」），如 `结论^1`。
 *       认出它靠「该段标题与它正下方那一列的列标题一字不差、且只跨 1 列」。</li>
 * </ul>
 * 纵向合并格必须用 rowspan 出、并且下层不再重复出这一格；否则「结论」二字会在上下两层各印一次。
 */
function planHeader(properties: QualityProps, columns: RenderColumn[]): HeaderPlan {
  const parsed = parseHeaderSpans(textOf(properties.headerSpans), columns.length);
  if (parsed.items.length === 0 || parsed.errors.length > 0) {
    // 没填或不合法：不出上层，表头退回单层
    return { cells: '', covered: new Set() };
  }
  const covered = new Set<number>();
  let lowerIndex = 0;
  const cells = parsed.items
    .map((span) => {
      const start = lowerIndex;
      lowerIndex += span.span;
      const vertical = span.span === 1 && columns[start]?.label === span.label;
      if (vertical) {
        covered.add(start);
      }
      const size = vertical ? 'rowspan="2"' : `colspan="${span.span}"`;
      return `<th ${size} style="${cssStyle({
        border: CELL_BORDER,
        padding: '6px 8px',
        background: '#fafafa',
        'font-weight': 600,
      })}">${escapeHtml(span.label)}</th>`;
    })
    .join('');
  return { cells, covered };
}

export const groupedQualityTableDefinition: QualityComponentDefinition = {
  type: 'GroupedQualityTable',
  name: 'groupedQualityTable',
  label: '分组检验项表',
  category: QUALITY_CATEGORY.INSPECTION,
  icon: QUALITY_ICONS.groupedTable,
  aiHint:
    '有分组层级的检验项表格：组名纵向合并、组内子项逐行展开（序号/检验项目/子项/检测要求/实测值/单位/判定）。' +
    '检验单里存在「一个检验项目下挂若干子项」这种两层结构时用它；检验项是平铺一层、没有父子关系时用 QualityTable。' +
    '原件这张表的**列**比上面列出的多（如还有「检测方法」「标准值」「结论」「备注」）时不要改用别的组件，' +
    '把全部列写进我的 columns 属性；表头分成上下两层时，把上面那一层写进 headerSpans。' +
    '某一列在原件里是「一格纵向合并跨住上下两层表头」（表现为下层表头那一格写着「↑同上」、' +
    '或该列名上下各出现一次）时，它不算任何分组标题的下属列，同样写进 headerSpans 但标题与列标题保持一致，' +
    '我会把它合成跨两行的一格。',
  defaults: {
    title: '检验项目',
    itemsPath: DEFAULT_ITEMS_PATH,
    showIndex: true,
    showRequirement: true,
    showActual: true,
    showUnit: true,
    showResult: true,
  },
  propertySchema: [
    { key: 'title', label: '表格标题', type: 'string', defaultValue: '检验项目' },
    {
      key: 'itemsPath',
      label: '检验项数据源',
      type: 'string',
      defaultValue: DEFAULT_ITEMS_PATH,
      required: true,
      bindable: true,
      // 渲染期按这个路径取数组逐行生成，必须是裸路径，不能写成 {{...}}
      bindAs: 'path',
      tip: '报告上下文中检验项目数组的路径，渲染时按它逐行生成。分组信息随每一项的下发，不需要另外指定',
    },
    { key: 'showIndex', label: '显示序号', type: 'boolean', defaultValue: true },
    { key: 'showRequirement', label: '显示检测要求', type: 'boolean', defaultValue: true },
    { key: 'showActual', label: '显示实测值', type: 'boolean', defaultValue: true },
    { key: 'showUnit', label: '显示单位', type: 'boolean', defaultValue: true },
    { key: 'showResult', label: '显示判定', type: 'boolean', defaultValue: true },
    {
      key: 'columns',
      label: '列定义（覆盖默认列）',
      type: 'text',
      tip:
        '留空则用上面的默认列。格式：列标题=绑定表达式，多列用 | 分隔，' +
        '如 序号={{index}}|检验项目={{item.itemName}}|检测方法={{item.checkMethod}}|实测值={{item.actualValue}}。' +
        '标题前加 # 表示该列纵向合并跨住整组（放组名的那一列）。' +
        '填了它整表就按它来，上面的显示开关不再生效',
    },
    {
      key: 'headerSpans',
      label: '表头跨列（两级表头才填）',
      type: 'text',
      tip:
        '只有表头分上下两层时才填，写的是上面那一层：每个单元格写作 标题^跨越列数，多段用 | 分隔，' +
        '如 检验结果^5|结论^1。下面那一层由列标题自动拼出，不要重复写。' +
        '各段跨列数之和必须等于列数。' +
        '某一列自己既是列名又是上格（原件里是一格纵向合并跨住上下两层）时，仍照写一段、跨列数写 1，' +
        '但该段标题要与这一列的列标题写成完全一样的字，报告会合出一格跨两行，不会上下各印一遍',
    },
  ],
  dataSchema: [
    { key: DEFAULT_ITEMS_PATH, label: '检验项目列表', type: 'string', bindable: true },
    { key: INDEX_VAR, label: '行序号', type: 'number', bindable: true },
    { key: `${LOOP_VAR}.itemName`, label: '检验项目名称', type: 'string', bindable: true },
    { key: `${LOOP_VAR}.group.childName`, label: '子项名称', type: 'string', bindable: true },
    { key: `${LOOP_VAR}.group.span`, label: '组名合并行数', type: 'number', bindable: true },
    { key: `${LOOP_VAR}.group.hidden`, label: '子项行隐藏合并格', type: 'string', bindable: true },
    { key: `${LOOP_VAR}.requirement`, label: '检测要求 / 组公式', type: 'string', bindable: true },
    { key: `${LOOP_VAR}.checkMethod`, label: '检测方法', type: 'string', bindable: true },
    { key: `${LOOP_VAR}.actualValue`, label: '实测值', type: 'string', bindable: true },
    { key: `${LOOP_VAR}.unit`, label: '单位', type: 'string', bindable: true },
    { key: `${LOOP_VAR}.resultText`, label: '判定结果', type: 'string', bindable: true },
  ],
  buildContent: (properties) => {
    const { columns, custom } = effectiveColumns(properties);
    const cellStyle = custom ? CUSTOM_CELL_STYLE : CELL_STYLE;
    const header = planHeader(properties, columns);
    const head = columns
      .map((column, index) =>
        header.covered.has(index)
          ? ''
          : `<th style="${cssStyle({
              border: CELL_BORDER,
              padding: '6px 8px',
              background: '#fafafa',
              'font-weight': 600,
              width: column.width,
            })}">${escapeHtml(column.label)}</th>`,
      )
      .join('');
    const row = columns
      .map(
        (column) =>
          `<td${column.merge ? GROUP_CELL_ATTRS : ''} style="${cellStyle}">${escapeHtml(column.expr)}</td>`,
      )
      .join('');
    const rawTitle = String(properties.title ?? '').trim();
    const heading = rawTitle
      ? `<div style="${cssStyle({ 'font-size': '13px', 'font-weight': 600, margin: '0 0 4px' })}">${escapeHtml(rawTitle)}</div>`
      : '';
    const itemsPath = String(properties.itemsPath ?? DEFAULT_ITEMS_PATH).trim() || DEFAULT_ITEMS_PATH;
    const table =
      `<table style="${cssStyle({ width: '100%', 'border-collapse': 'collapse', 'font-size': '12px' })}">` +
      `<thead>${header.cells ? `<tr>${header.cells}</tr>` : ''}<tr>${head}</tr></thead>` +
      // 渲染期按 data-qc-repeat 指向的数组复制 data-qc-repeat-row 所在的行；
      // 组头行与组内子项行在这套结构里都是数组里的一项，合并靠组名格自带的 rowspan
      `<tbody ${QUALITY_REPEAT_ATTR}="${escapeHtml(itemsPath)}">` +
      `<tr ${QUALITY_REPEAT_ROW_ATTR}="${LOOP_VAR}">${row}</tr>` +
      `</tbody></table>`;
    return {
      tagName: 'div',
      components: heading + table,
      style: { margin: '0 0 10px' },
    };
  },
  validate: (properties) => {
    const errors: string[] = [];
    const path = textOf(properties.itemsPath);
    if (!path) {
      errors.push('分组检验项表的「检验项数据源」不能为空，渲染时无法确定生成哪些行');
    }

    const parsed = parseColumnSpec(textOf(properties.columns));
    errors.push(...parsed.errors);
    const customColumns = parsed.items.length > 0;

    // 显示开关只在用默认列时说了算：填了 columns 整表就按 columns 来，
    // 「隐藏了检测要求却保留判定」在这里不再成立，报了就是误报
    if (!customColumns && properties.showRequirement === false && properties.showResult === true) {
      errors.push(
        '分组检验项表隐藏了「检测要求」却保留「判定」列，判定依据将不可见，请一并隐藏判定或恢复检测要求',
      );
    }

    const headerSpec = textOf(properties.headerSpans);
    // 跨列数的合计校验要先知道实际列数，而列定义本身有错时列数不可信：
    // 此时报「合计 N，实际列数 M」会把用户往错误的方向带，先只报列定义的错
    if (headerSpec && parsed.errors.length === 0) {
      const columnCount = (customColumns ? parsed.items : defaultColumns(properties)).length;
      errors.push(...parseHeaderSpans(headerSpec, columnCount).errors);
    }
    return errors;
  },
};
