/**
 * 报告渲染引擎
 * <p>
 * 输入「模板 Schema + 报告上下文」，输出可直接打印的 HTML 文档。
 * 渲染顺序固定为「先判定、后渲染」：PASS/FAIL 由 report-evaluator 算好写进上下文，
 * 模板里只做取值，因此同一份数据在任何模板下判定一致，模板也无法左右判定结果。
 * <p>
 * 这里不依赖 GrapesJS，只吃 GrapesJS 导出的项目数据（组件树 + 样式），
 * 后续换成服务端渲染时这套投影逻辑可以照搬。
 */
import type { ReportContext } from './context';
import type { QualityRuleDefinition } from './report-evaluator';

import {
  QUALITY_ID_ATTR,
  QUALITY_PROP_PREFIX,
  QUALITY_REPEAT_ATTR,
  QUALITY_REPEAT_ROW_ATTR,
  QUALITY_TYPE_ATTR,
} from '../core/attrs';
import { cssStyle, escapeHtml } from '../core/html';

import { resolveTextWithReport } from './binding';
import type { PageSetting } from './page';
import { readPath } from './path';
import { evaluateReport } from './report-evaluator';
import { resolvePageMm } from './page';

/** 渲染用到的 GrapesJS 项目数据节点（只声明渲染需要的字段） */
export interface RenderNode {
  attributes?: Record<string, string>;
  components?: RenderNode[] | string;
  content?: string;
  style?: Record<string, string>;
  tagName?: string;
  type?: string;
}

/** GrapesJS 导出的一条样式规则 */
export interface RenderStyleRule {
  selectors?: string[];
  style?: Record<string, string>;
  /** @media 的查询条件，理论上模板不含媒体查询（画布设备已置空 widthMedia） */
  mediaText?: string;
}

/** 渲染用到的 Schema 结构，结构等价于 ReportTemplateSchema，不依赖具体接口类型定义 */
export interface RenderSchema {
  page?: PageSetting;
  grapes?: {
    pages?: Array<{ frames?: Array<{ component?: RenderNode }> }>;
    /** 新数据是规则数组，历史数据可能是 CSS 文本 */
    styles?: RenderStyleRule[] | string;
  };
}

export interface RenderOptions {
  schema?: RenderSchema;
  context: ReportContext;
  rules?: QualityRuleDefinition[];
}

export interface RenderOutcome {
  /** 完整 HTML 文档，打印/PDF 直接使用 */
  html: string;
  /** 正文片段，便于嵌入页面预览 */
  body: string;
  /** 判定后的上下文（含 PASS/FAIL 判定与合格率） */
  context: ReportContext;
  /** 规则与绑定的问题清单，调用方必须显式暴露，不能悄悄吞掉 */
  errors: string[];
}

/** 渲染作用域：判定后的报告上下文，外加循环变量（如 item）与序号 index */
type RenderScope = Record<string, unknown>;

/** 一次渲染的行走状态：作用域 + 当前位置 + 问题清单，按引用在递归里共享 */
interface RenderState {
  scope: RenderScope;
  /** 当前渲染位置的描述，用于把问题定位到具体行 */
  where: string;
  errors: string[];
  /** 已上报的缺口，同一处只提示一次 */
  reported: Set<string>;
}

/** 自闭合标签，不能生成结束标签 */
const VOID_TAGS = new Set([
  'br',
  'col',
  'hr',
  'img',
  'input',
  'link',
  'meta',
  'source',
  'track',
  'wbr',
]);

/**
 * GrapesJS 组件类型 → HTML 标签。
 * <p>
 * 项目数据只在标签与类型默认值不同时才写 tagName（例如 th、span、h2），
 * 表格结构（table/thead/tbody/row/cell）在数据里都只有 type，
 * 所以漏掉这张表就会把整张表格渲染成一堆 div。未收录的类型回退到 div，
 * 与 GrapesJS 基础组件的默认标签一致。
 */
const TYPE_TAGS: Record<string, string> = {
  cell: 'td',
  row: 'tr',
  table: 'table',
  tbody: 'tbody',
  tfoot: 'tfoot',
  thead: 'thead',
  text: 'div',
  wrapper: 'div',
};

/** 渲染产物的基础排版。浏览器默认的表格与页边距会让报告走样，这里收敛成打印友好的基线 */
const BASE_CSS = [
  '* { box-sizing: border-box; }',
  'body { margin: 0; color: #000; font-family: "Microsoft YaHei", "PingFang SC", sans-serif; font-size: 12px; line-height: 1.5; }',
  'table { border-collapse: collapse; width: 100%; }',
  'img { max-width: 100%; }',
  'tr, td, th { page-break-inside: avoid; }',
].join('\n');

/** 渲染报告：判定 → 生成正文 → 拼样式 → 组装文档 */
export function renderReport(options: RenderOptions): RenderOutcome {
  const { context, errors } = evaluateReport(options.context, options.rules ?? []);
  const state: RenderState = {
    scope: { ...context },
    where: '模板',
    errors,
    reported: new Set<string>(),
  };

  const root = options.schema?.grapes?.pages?.[0]?.frames?.[0]?.component;
  const body = root ? renderNode(root, state) : '';
  const css = buildCss(options.schema?.grapes?.styles);
  const html = buildDocument(options.schema?.page, body, css, context);
  return { html, body, context, errors };
}

/** 渲染单个节点：文本节点与重复容器单独处理，其余按「标签 + 属性 + 子节点」展开 */
function renderNode(node: RenderNode, state: RenderState): string {
  // 文本节点只贡献文本，自身不产生标签
  if (node.type === 'textnode') {
    return renderText(node.content ?? '', state);
  }

  const repeatPath = node.attributes?.[QUALITY_REPEAT_ATTR];
  if (repeatPath) {
    return renderRepeat(node, repeatPath, state);
  }

  const tag = resolveTag(node);
  const attributes = renderAttributes(node, state);
  const style = renderStyle(node);
  if (VOID_TAGS.has(tag)) {
    return `<${tag}${attributes}${style}>`;
  }
  return `<${tag}${attributes}${style}>${renderChildren(node, state)}</${tag}>`;
}

/** 标签优先取数据里的 tagName，其次按组件类型推断 */
function resolveTag(node: RenderNode): string {
  return node.tagName ?? TYPE_TAGS[node.type ?? ''] ?? 'div';
}

/**
 * 子节点按声明顺序展开。
 * <p>
 * 有子组件时忽略 content——与 GrapesJS 一致：往一个带文本的组件里再拖入组件后，
 * 文本就让位给子组件，这里必须同规则，否则渲染产物会和设计器看到的不一样。
 */
function renderChildren(node: RenderNode, state: RenderState): string {
  if (Array.isArray(node.components) && node.components.length > 0) {
    return node.components.map((child) => renderNode(child, state)).join('');
  }
  return typeof node.content === 'string' ? renderText(node.content, state) : '';
}

/**
 * 重复容器：按数组路径展开行模板，其余子节点在原位置渲染一次。
 * <p>
 * 行模板上的 data-qc-repeat-row 声明了行内的循环变量名（如 item），
 * 行内容用 {{item.xxx}} 取值，与设计期在组件里约定的绑定路径一致。
 */
function renderRepeat(node: RenderNode, path: string, state: RenderState): string {
  const list = toRowList(readPath(state.scope, path));
  const children = Array.isArray(node.components) ? node.components : [];
  const inner = children
    .map((child) => {
      const rowVar = child.attributes?.[QUALITY_REPEAT_ROW_ATTR];
      if (!rowVar) {
        return renderNode(child, state);
      }
      return list
        .map((element, index) => {
          const rowState: RenderState = {
            ...state,
            scope: { ...state.scope, [rowVar]: element, index: index + 1 },
            where: `第 ${index + 1} 行`,
          };
          return renderNode(child, rowState);
        })
        .join('');
    })
    .join('');

  const tag = resolveTag(node);
  return `<${tag}${renderAttributes(node, state)}${renderStyle(node)}>${inner}</${tag}>`;
}

/** 非数组按单行处理，空值得到空表；比抛错更贴合"数据没填全"的报告场景 */
function toRowList(value: unknown): unknown[] {
  if (Array.isArray(value)) {
    return value;
  }
  if (value === undefined || value === null) {
    return [];
  }
  return [value];
}

/**
 * 渲染节点属性。
 * <p>
 * 设计器标记（data-quality-type / data-qc-*）不进入产物；
 * 属性值里的 {{path}} 一并解析，图片地址、链接等也能绑定数据。
 */
function renderAttributes(node: RenderNode, state: RenderState): string {
  const attributes = node.attributes ?? {};
  const parts: string[] = [];
  Object.entries(attributes).forEach(([name, value]) => {
    if (name === QUALITY_TYPE_ATTR || name.startsWith(QUALITY_PROP_PREFIX)) {
      return;
    }
    const text = renderText(String(value ?? ''), state);
    if (text === '') {
      return;
    }
    // GrapesJS 的组件 id 换成 data 属性：重复行里 id 会重名，data 属性不会
    parts.push(` ${name === 'id' ? QUALITY_ID_ATTR : name}="${escapeHtml(text)}"`);
  });
  return parts.join('');
}

/** 节点自身的行内样式 */
function renderStyle(node: RenderNode): string {
  const style = cssStyle(node.style ?? {});
  return style ? ` style="${escapeHtml(style)}"` : '';
}

/** 取文本里的绑定值，取不到的路径记进问题清单（未解析的 {{}} 不会留在产物里） */
function renderText(template: string, state: RenderState): string {
  return resolveTextWithReport(template, state.scope, (path) => {
    const key = `${state.where}|${path}`;
    if (state.reported.has(key)) {
      return;
    }
    state.reported.add(key);
    state.errors.push(`${state.where}绑定「{{${path}}}」在当前数据中取不到值，已渲染为空`);
  });
}

/** 样式规则数组 → CSS 文本；GrapesJS 用 #组件id 选择器，这里改写成 data 属性选择器 */
function buildCss(styles: RenderStyleRule[] | string | undefined): string {
  if (typeof styles === 'string') {
    return styles;
  }
  if (!Array.isArray(styles)) {
    return '';
  }
  const blocks: string[] = [];
  const mediaBlocks = new Map<string, string[]>();

  styles.forEach((rule) => {
    const selector = (rule.selectors ?? []).map(toSafeSelector).filter(Boolean).join(', ');
    const body = cssStyle(rule.style ?? {});
    if (!selector || !body) {
      return;
    }
    const line = `${selector} { ${body} }`;
    const mediaText = rule.mediaText?.trim();
    if (mediaText) {
      mediaBlocks.set(mediaText, [...(mediaBlocks.get(mediaText) ?? []), line]);
      return;
    }
    blocks.push(line);
  });

  mediaBlocks.forEach((lines, mediaText) => {
    const atRule = mediaText.startsWith('@') ? mediaText : `@media ${mediaText}`;
    blocks.push(`${atRule} { ${lines.join(' ')} }`);
  });
  return blocks.join('\n');
}

/** #组件id → [data-qc-id="组件id"]，重复行复制后样式仍然命中 */
function toSafeSelector(selector: string): string {
  return selector.replaceAll(/#([\w-]+)/g, `[${QUALITY_ID_ATTR}="$1"]`);
}

/** 组装最终文档：纸张与页边距交给 @page，正文里不再重复留白，才能保证每页都有边距 */
function buildDocument(
  page: PageSetting | undefined,
  body: string,
  css: string,
  context: ReportContext,
): string {
  const { heightMm, marginMm, widthMm } = resolvePageMm(page);
  const size = `${widthMm}mm ${heightMm}mm`;
  const margin = `${marginMm.top}mm ${marginMm.right}mm ${marginMm.bottom}mm ${marginMm.left}mm`;
  const title = context.report.reportName || context.report.reportNo || '质检报告';
  return [
    '<!doctype html>',
    '<html lang="zh-CN">',
    '<head>',
    '<meta charset="utf-8" />',
    `<title>${escapeHtml(title)}</title>`,
    '<style>',
    `@page { size: ${size}; margin: ${margin}; }`,
    BASE_CSS,
    css,
    '</style>',
    '</head>',
    `<body>${body}</body>`,
    '</html>',
  ].join('\n');
}