"""
使用Jinja2语法的Symphra Excel库模板引擎
"""

import io
import re
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable, Iterable

from jinja2 import BaseLoader, Environment
from jinja2.runtime import Undefined
from jinja2.sandbox import SandboxedEnvironment

from ..core.workbook import Workbook
from ..core.worksheet import Worksheet
from ..images.image_processor import CellImageOptions
from ..utils.exceptions import TemplateError, handle_exceptions
from ..utils.helpers import (
    column_letter_to_number,
    safe_convert_to_number,
    validate_file_path,
)


class SilentUndefined(Undefined):
    """静默未定义变量处理器"""

    def __str__(self) -> str:
        return ""

    def __getattr__(self, name: str) -> Any:
        return SilentUndefined()

    def __call__(self, *args: Any, **kwargs: Any) -> Any:
        return SilentUndefined()


class ExcelTemplateLoader(BaseLoader):
    """Excel模板的自定义模板加载器"""

    def __init__(self, workbook: Workbook) -> None:
        self._workbook = workbook

    def get_source(
        self, environment: Environment, template: str
    ) -> tuple[str, str | None, Callable[[], bool] | None]:
        worksheet = self._workbook.get_sheet_by_name(template)
        if worksheet is None:
            raise TemplateError(f"未找到模板工作表 '{template}'")

        used_range = worksheet.get_used_range()
        values = worksheet.get_range_values(used_range)

        lines: list[str] = []
        for row in values:
            line = "\t".join("" if value is None else str(value) for value in row)
            lines.append(line)
        source = "\n".join(lines)
        return source, template, lambda: True


@dataclass
class ImagePlaceholder:
    """保存延迟图像插入的元数据"""

    source: str | Path | bytes | io.BytesIO
    width: int | None = None
    height: int | None = None
    options: CellImageOptions | None = None
    fit_cell: bool = False  # 是否适应单元格大小
    keep_original_size: bool = False  # 是否保持原始大小


class TemplateWorkbook(Workbook):
    """
    支持模板的Workbook类，使用Jinja2语法
    """

    ALLOWED_FILTERS: set[str] = {
        "default",
        "length",
        "upper",
        "lower",
        "title",
        "capitalize",
        "replace",
        "trim",
        "striptags",
        "join",
        "round",
    }

    def __init__(self, template_file: str | Path | None = None) -> None:
        """
        初始化模板工作簿

        参数:
            template_file: 可选的要加载的模板文件
        """
        super().__init__(template_file)
        self._template_environment: SandboxedEnvironment | None = None
        self._template_variables: set[str] = set()
        self._template_file = template_file
        self._image_counter: int = 0
        self._current_image_placeholders: dict[str, ImagePlaceholder] = {}

        if template_file:
            self._scan_template_variables()

    def _scan_template_variables(self) -> None:
        """
        扫描工作簿中的模板变量
        """
        template_pattern = re.compile(r"\{\{.*?\}\}|\{%.*?%\}|\{#.*?#\}")

        for worksheet in self.worksheets:
            used_range = worksheet.get_used_range()
            values = worksheet.get_range_values(used_range)

            for row in values:
                for cell_value in row:
                    if isinstance(cell_value, str) and template_pattern.search(
                        cell_value
                    ):
                        # 从模板语法中提取变量名
                        self._extract_variables(cell_value)

    def _extract_variables(self, template_text: str) -> None:
        """
        从文本中提取模板变量

        参数:
            template_text: 要扫描的模板文本
        """
        # 处理简单变量模式如 {{ variable }}
        simple_pattern = re.compile(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}")
        simple_matches = simple_pattern.findall(template_text)
        for var_name in simple_matches:
            self._template_variables.add(var_name)

        # 处理嵌套变量模式如 {{ items[0].name }}
        nested_pattern = re.compile(
            r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\[.*?\]\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}"
        )
        nested_matches = nested_pattern.findall(template_text)
        for parent_var, _ in nested_matches:
            self._template_variables.add(parent_var)

        # 处理属性访问模式如 {{ object.attribute }}
        attr_pattern = re.compile(
            r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}"
        )
        attr_matches = attr_pattern.findall(template_text)
        for parent_var, _ in attr_matches:
            self._template_variables.add(parent_var)

        # 还要查找循环变量和其他结构
        loop_pattern = re.compile(r"\{%\s*for\s+(\w+)\s+in\s+(\w+)\s*%\}")
        loop_matches = loop_pattern.findall(template_text)
        for var_name, _ in loop_matches:
            self._template_variables.add(var_name)

    @handle_exceptions(
        error_type=TemplateError,
        error_message="模板渲染失败",
    )
    def render(
        self, context: dict[str, Any], worksheet_name: str | None = None
    ) -> "TemplateWorkbook":
        """
        使用提供的上下文渲染模板

        参数:
            context: 模板上下文数据
            worksheet_name: 要渲染的特定工作表（可选）

        返回:
            渲染后的工作簿实例

        异常:
            TemplateError: 如果模板渲染失败
        """
        self._ensure_safe_context(context)

        self._image_counter = 0
        if worksheet_name:
            # 渲染特定工作表
            worksheet = self.get_sheet_by_name(worksheet_name)
            if worksheet is None:
                raise TemplateError(f"未找到工作表 '{worksheet_name}'")
            self._render_worksheet(worksheet, context)
        else:
            # 渲染所有工作表
            for worksheet in self.worksheets:
                self._render_worksheet(worksheet, context)

        return self

    def _render_worksheet(self, worksheet: Worksheet, context: dict[str, Any]) -> None:
        """
        渲染工作表，保留原始样式和合并单元格

        参数:
            worksheet: 要渲染的工作表
            context: 模板上下文
        """
        self._current_image_placeholders.clear()

        used_range = worksheet.get_used_range()
        values = worksheet.get_range_values(used_range)

        # 保存原始样式、合并单元格和图片
        original_styles = self._save_worksheet_styles(worksheet, len(values))
        original_merges = self._save_merged_cells(worksheet)
        original_images = self._save_worksheet_images(worksheet)

        plan, _, _ = self._build_render_plan(values, 0)
        rendered_rows, row_mapping = self._execute_plan_with_mapping(
            plan, context, values
        )

        # 清空原有内容
        max_row = worksheet.max_row
        if max_row > 0:
            worksheet._worksheet.delete_rows(1, max_row)
            # 清空合并单元格（delete_rows 不会自动清空）
            worksheet._worksheet.merged_cells.ranges.clear()
        worksheet.clear_cached_styles()

        # 写入渲染后的数据并恢复样式和合并单元格
        for row_idx, row_data in enumerate(rendered_rows):
            worksheet.append_row(row_data)
            # 恢复该行的样式
            self._restore_row_styles_mapped(
                worksheet, row_idx + 1, original_styles, row_mapping[row_idx]
            )

        # 恢复合并单元格
        self._restore_merged_cells(worksheet, original_merges, row_mapping)

        # 恢复原有图片
        self._restore_worksheet_images(worksheet, original_images, row_mapping)

        self._apply_image_placeholders(worksheet)

    def _save_worksheet_styles(self, worksheet: Worksheet, num_rows: int) -> dict:
        """
        保存工作表样式信息

        参数:
            worksheet: 工作表对象
            num_rows: 行数

        返回:
            样式信息字典，格式: {row_idx: {col_idx: {style_attrs}}}
        """
        from openpyxl.styles import Font, Border, Alignment, Protection, PatternFill

        styles = {}
        ws = worksheet._worksheet

        for row_idx in range(1, num_rows + 1):
            row_styles = {}
            for cell in ws[row_idx]:
                col_idx = cell.column

                # 复制字体
                font = None
                if cell.font:
                    font = Font(
                        name=cell.font.name,
                        size=cell.font.size,
                        bold=cell.font.bold,
                        italic=cell.font.italic,
                        vertAlign=cell.font.vertAlign,
                        underline=cell.font.underline,
                        strike=cell.font.strike,
                        color=cell.font.color,
                    )

                # 复制边框
                border = None
                if cell.border:
                    border = Border(
                        left=cell.border.left,
                        right=cell.border.right,
                        top=cell.border.top,
                        bottom=cell.border.bottom,
                        diagonal=cell.border.diagonal,
                        diagonal_direction=cell.border.diagonal_direction,
                    )

                # 复制填充
                fill = None
                if (
                    cell.fill
                    and hasattr(cell.fill, "patternType")
                    and cell.fill.patternType
                ):
                    fill = PatternFill(
                        patternType=cell.fill.patternType,
                        fgColor=cell.fill.fgColor,
                        bgColor=cell.fill.bgColor,
                    )

                # 复制对齐
                alignment = None
                if cell.alignment:
                    alignment = Alignment(
                        horizontal=cell.alignment.horizontal,
                        vertical=cell.alignment.vertical,
                        text_rotation=cell.alignment.text_rotation,
                        wrap_text=cell.alignment.wrap_text,
                        shrink_to_fit=cell.alignment.shrink_to_fit,
                        indent=cell.alignment.indent,
                    )

                # 复制保护
                protection = None
                if cell.protection:
                    protection = Protection(
                        locked=cell.protection.locked, hidden=cell.protection.hidden
                    )

                row_styles[col_idx] = {
                    "font": font,
                    "border": border,
                    "fill": fill,
                    "alignment": alignment,
                    "number_format": cell.number_format,
                    "protection": protection,
                }

            # 保存行高信息
            row_height = None
            if row_idx in ws.row_dimensions:
                row_height = ws.row_dimensions[row_idx].height

            # 组织样式信息
            styles[row_idx] = {"row_height": row_height, "cells": row_styles}

        return styles

    def _restore_row_styles_mapped(
        self,
        worksheet: Worksheet,
        target_row: int,
        original_styles: dict,
        source_row: int,
    ) -> None:
        """
        根据映射关系恢复行样式

        参数:
            worksheet: 工作表对象
            target_row: 目标行号（1-based）
            original_styles: 原始样式信息
            source_row: 原始模板行号（1-based）
        """
        if source_row not in original_styles:
            return

        ws = worksheet._worksheet
        row_styles = original_styles[source_row]

        # 恢复行高
        if "row_height" in row_styles and row_styles["row_height"]:
            ws.row_dimensions[target_row].height = row_styles["row_height"]

        # 恢复单元格样式
        if "cells" in row_styles:
            for col_idx, style_info in row_styles["cells"].items():
                cell = ws.cell(row=target_row, column=col_idx)
                if style_info.get("font"):
                    cell.font = style_info["font"]
                if style_info.get("border"):
                    cell.border = style_info["border"]
                if style_info.get("fill"):
                    cell.fill = style_info["fill"]
                if style_info.get("alignment"):
                    cell.alignment = style_info["alignment"]
                if style_info.get("number_format"):
                    cell.number_format = style_info["number_format"]
                if style_info.get("protection"):
                    cell.protection = style_info["protection"]

    def render_worksheet(
        self, worksheet_name: str, context: dict[str, Any]
    ) -> Worksheet:
        """
        渲染指定名称的工作表

        参数:
            worksheet_name: 目标工作表名称
            context: 模板上下文

        返回:
            渲染后的工作表
        """
        worksheet = self.get_sheet_by_name(worksheet_name)
        if worksheet is None:
            raise TemplateError(f"未找到工作表 '{worksheet_name}'")
        self._render_worksheet(worksheet, context)
        return worksheet

    def _render_template_text(self, template_text: Any, context: dict[str, Any]) -> Any:
        """
        使用上下文渲染模板文本

        参数:
            template_text: 要渲染的模板文本
            context: 模板上下文

        返回:
            渲染后的文本
        """
        if not isinstance(template_text, str):
            return template_text

        if not self._template_environment:
            self._template_environment = self._create_environment()

        template = self._template_environment.from_string(template_text)
        return template.render(**context)

    def _has_template_syntax(self, text: str) -> bool:
        """
        检查文本是否包含Jinja2模板语法

        参数:
            text: 要检查的文本

        返回:
            如果包含模板语法则返回True
        """
        return bool(re.search(r"\{\{.*?\}\}|\{%.*?%\}|\{#.*?#\}", text))

    def _get_column_letter(self, column_number: int) -> str:
        """
        Convert column number to letter.

        Args:
            column_number: Column number (1-based)

        Returns:
            Column letter
        """
        letter = ""
        while column_number > 0:
            column_number -= 1
            letter = chr(ord("A") + column_number % 26) + letter
            column_number //= 26
        return letter

    def _process_template_constructs(
        self, worksheet: Worksheet, context: dict[str, Any]
    ) -> None:
        """
        Compatibility shim retained for API; actual处理在 _build_render_plan/_execute_plan.
        """
        return

    def get_worksheet(self, name: str) -> Worksheet | None:
        """
        Get worksheet by name (alias for get_sheet_by_name).

        Args:
            name: Worksheet name

        Returns:
            Worksheet object or None if not found
        """
        return self.get_sheet_by_name(name)

    def get_template_variables(self) -> set[str]:
        """
        Get all template variables found in the workbook.

        Returns:
            Set of template variable names
        """
        return self._template_variables.copy()

    def validate_context(self, context: dict[str, Any]) -> list[str]:
        """
        Validate template context.

        Args:
            context: Template context to validate

        Returns:
            List of missing variables
        """
        missing_vars = []
        for var_name in self._template_variables:
            if var_name not in context:
                missing_vars.append(var_name)
        return missing_vars

    def _create_environment(self) -> SandboxedEnvironment:
        """
        Create a sandboxed Jinja2 environment with helper functions.
        """
        environment = SandboxedEnvironment(
            loader=ExcelTemplateLoader(self),
            undefined=SilentUndefined,
            trim_blocks=True,
            lstrip_blocks=True,
        )
        self._apply_allowed_filters(environment)
        environment.globals["insert_image"] = self._build_image_helper()
        return environment

    def _build_image_helper(self) -> Callable[..., str]:
        """
        Build helper callable exposed to templates for image insertion.
        """

        def _helper(source: str | Path | bytes | io.BytesIO, **kwargs: Any) -> str:
            return self._register_image_placeholder(source, **kwargs)

        return _helper

    def _register_image_placeholder(
        self, source: str | Path | bytes | io.BytesIO, **kwargs: Any
    ) -> str:
        """
        Register image placeholder and return token for template replacement.
        """
        token = f"__SYMPHRA_IMAGE_{self._image_counter}__"
        self._image_counter += 1

        width = kwargs.pop("width", None)
        height = kwargs.pop("height", None)
        fit_cell = kwargs.pop("fit_cell", False)  # 提取 fit_cell 参数
        keep_original_size = kwargs.pop(
            "keep_original_size", False
        )  # 提取 keep_original_size 参数

        options_kwargs = {
            key: kwargs[key]
            for key in [
                "anchor_to_cell",
                "move_with_cells",
                "resize_with_cells",
                "maintain_aspect_ratio",
                "max_width",
                "max_height",
                "position",
                "margin",
            ]
            if key in kwargs
        }

        # 如果是 fit_cell 模式，设置默认的 options
        if fit_cell and not options_kwargs:
            options_kwargs = {
                "anchor_to_cell": True,
                "move_with_cells": True,
                "resize_with_cells": True,
                "maintain_aspect_ratio": True,
                "position": "center",
                "margin": 2,
            }

        options = CellImageOptions(**options_kwargs) if options_kwargs else None

        if isinstance(source, (str, Path)):
            source = str(validate_file_path(source))

        self._current_image_placeholders[token] = ImagePlaceholder(
            source=source,
            width=width,
            height=height,
            options=options,
            fit_cell=fit_cell,
            keep_original_size=keep_original_size,
        )
        return token

    def _apply_image_placeholders(self, worksheet: Worksheet) -> None:
        """
        Insert images for placeholders rendered in the worksheet.
        """
        if not self._current_image_placeholders:
            return

        used_range = worksheet.get_used_range()
        values = worksheet.get_range_values(used_range)

        for row_idx, row in enumerate(values):
            for col_idx, cell_value in enumerate(row):
                if (
                    isinstance(cell_value, str)
                    and cell_value in self._current_image_placeholders
                ):
                    placeholder = self._current_image_placeholders[cell_value]
                    cell_ref = f"{self._get_column_letter(col_idx + 1)}{row_idx + 1}"
                    worksheet.set_value(cell_ref, None)

                    # 根据不同模式插入图片
                    if placeholder.fit_cell:
                        # 适配单元格大小模式 - 使用嵌入单元格样式
                        worksheet.insert_image_in_cell(
                            placeholder.source,
                            cell_ref,
                            options=placeholder.options,
                        )
                    elif (
                        hasattr(placeholder, "keep_original_size")
                        and placeholder.keep_original_size
                    ):
                        # 保持原始大小模式
                        worksheet.insert_image_in_cell(
                            placeholder.source,
                            cell_ref,
                            options=placeholder.options,
                        )
                    else:
                        # 指定尺寸模式
                        worksheet.insert_image(
                            placeholder.source,
                            cell_ref,
                            width=placeholder.width,
                            height=placeholder.height,
                            options=placeholder.options,
                        )

        self._current_image_placeholders.clear()

    def add_template_row(
        self, worksheet_name: str, template_row: int, data_list: list[dict[str, Any]]
    ) -> None:
        """
        Add template rows based on data list.

        Args:
            worksheet_name: Target worksheet name
            template_row: Row number containing template (1-based)
            data_list: List of data dictionaries
        """
        worksheet = self.get_sheet_by_name(worksheet_name)
        if worksheet is None:
            raise TemplateError(f"Worksheet '{worksheet_name}' not found")

        # Get template row data
        template_values = []
        max_col = worksheet.max_column

        for col in range(1, max_col + 1):
            cell_ref = f"{self._get_column_letter(col)}{template_row}"
            template_values.append(worksheet.get_value(cell_ref))

        # Insert and render data rows
        for data_idx, data_item in enumerate(data_list):
            # Insert new row
            if data_idx > 0:
                new_row = template_row + data_idx
                worksheet.insert_rows(new_row)

                # 复制样式到新行 (行高、列宽等)
                self._copy_row_styles(worksheet, template_row, new_row)

            # Render template for this row
            for col_idx, template_value in enumerate(template_values):
                if isinstance(template_value, str) and self._has_template_syntax(
                    template_value
                ):
                    rendered_value = self._render_template_text(
                        template_value, data_item
                    )
                    cell_ref = f"{self._get_column_letter(col_idx + 1)}{template_row + data_idx}"
                    worksheet.set_value(cell_ref, rendered_value)

    def render_table(
        self,
        worksheet_name: str,
        start_cell: str,
        headers: list[str],
        data: list[list[Any]],
        context: dict[str, Any] | None = None,
    ) -> None:
        """
        Render a table with headers and data.

        Args:
            worksheet_name: Target worksheet name
            start_cell: Top-left cell of table (e.g., 'A1')
            headers: List of column headers
            data: List of data rows
            context: Optional template context
        """
        worksheet = self.get_sheet_by_name(worksheet_name)
        if worksheet is None:
            raise TemplateError(f"Worksheet '{worksheet_name}' not found")

        from ..utils.helpers import validate_cell_reference

        start_col, start_row = validate_cell_reference(start_cell)
        start_col_index = column_letter_to_number(start_col)

        # Render headers
        for col_idx, header in enumerate(headers):
            col_letter = self._get_column_letter(start_col_index + col_idx)
            cell_ref = f"{col_letter}{start_row}"

            if (
                context
                and isinstance(header, str)
                and self._has_template_syntax(header)
            ):
                header = self._render_template_text(header, context)

            worksheet.set_value(cell_ref, header)

        # Render data
        for row_idx, row_data in enumerate(data):
            for col_idx, cell_data in enumerate(row_data):
                col_letter = self._get_column_letter(start_col_index + col_idx)
                cell_ref = f"{col_letter}{start_row + row_idx + 1}"

                if (
                    context
                    and isinstance(cell_data, str)
                    and self._has_template_syntax(cell_data)
                ):
                    cell_data = self._render_template_text(cell_data, context)

                worksheet.set_value(cell_ref, cell_data)

    def create_from_template(
        self,
        template_path: str | Path,
        output_path: str | Path,
        context: dict[str, Any],
    ) -> None:
        """
        Create workbook from template file and save to output.

        Args:
            template_path: Path to template file
            output_path: Path to save output file
            context: Template context data
        """
        # Load template
        template_wb = TemplateWorkbook(template_path)

        # Render with context
        template_wb.render(context)

        # Save to output
        template_wb.save(output_path)

    @staticmethod
    def load_template(template_path: str | Path) -> "TemplateWorkbook":
        """
        Load template from file.

        Args:
            template_path: Path to template file

        Returns:
            TemplateWorkbook instance
        """
        return TemplateWorkbook(template_path)

    @staticmethod
    def create_simple_template(
        template_path: str | Path, variables: dict[str, Any]
    ) -> None:
        """
        Create a simple template file with variables.

        Args:
            template_path: Path to create template
            variables: Dictionary of variable names and example values
        """
        wb = Workbook()
        ws = wb.active
        ws.name = "Template"

        # Add header
        ws.set_value("A1", "Template Variables")
        ws.set_value("B1", "Values")

        # Add variables
        for idx, (var_name, example_value) in enumerate(variables.items(), start=2):
            ws.set_value(f"A{idx}", f"{{{{ {var_name} }}}}")
            ws.set_value(f"B{idx}", str(example_value))

        wb.save(template_path)

    # ------------------------------------------------------------------ #
    # Internal helpers
    # ------------------------------------------------------------------ #
    def _build_render_plan(
        self,
        rows: list[list[Any]],
        start_idx: int,
        end_tokens: set[str] | None = None,
    ) -> tuple[list[dict[str, Any]], int, str | None]:
        plan: list[dict[str, Any]] = []
        idx = start_idx
        total = len(rows)

        while idx < total:
            row = rows[idx]
            marker = self._extract_marker(row)

            if marker:
                stripped = marker.strip()
                if end_tokens and stripped in end_tokens:
                    return plan, idx + 1, stripped

                if stripped.startswith("{% for"):
                    match = re.match(r"\{% for\s+(\w+)\s+in\s+(.*?)\s*%\}", stripped)
                    if not match:
                        raise TemplateError(
                            f"Invalid for loop syntax at row {idx + 1}: {stripped}"
                        )
                    var_name, iterable_expr = match.groups()
                    # 循环标记行的行号
                    loop_marker_row = idx + 1
                    body_plan, new_idx, closing = self._build_render_plan(
                        rows,
                        idx + 1,
                        end_tokens={"{% endfor %}"},
                    )
                    if closing != "{% endfor %}":
                        raise TemplateError("Missing {% endfor %} for loop")
                    # 循环体的源行号（循环标记的下一行）
                    body_source_row = loop_marker_row + 1
                    plan.append(
                        {
                            "type": "loop",
                            "var": var_name.strip(),
                            "expr": iterable_expr.strip(),
                            "body": body_plan,
                            "body_source_row": body_source_row,
                        }
                    )
                    idx = new_idx
                    continue

                if stripped.startswith("{% if"):
                    match = re.match(r"\{% if\s+(.*?)\s*%\}", stripped)
                    if not match:
                        raise TemplateError(
                            f"Invalid if syntax at row {idx + 1}: {stripped}"
                        )
                    condition_expr = match.group(1).strip()

                    true_plan, new_idx, closing = self._build_render_plan(
                        rows,
                        idx + 1,
                        end_tokens={"{% else %}", "{% endif %}"},
                    )

                    false_plan: list[dict[str, Any]] = []
                    if closing == "{% else %}":
                        false_plan, new_idx, closing = self._build_render_plan(
                            rows,
                            new_idx,
                            end_tokens={"{% endif %}"},
                        )

                    if closing != "{% endif %}":
                        raise TemplateError("Missing {% endif %} for if statement")

                    plan.append(
                        {
                            "type": "if",
                            "expr": condition_expr,
                            "true": true_plan,
                            "false": false_plan,
                        }
                    )
                    idx = new_idx
                    continue

                # Unknown marker, skip
                idx += 1
                continue

            plan.append(
                {
                    "type": "row",
                    "cells": row,
                    "source_row": idx + 1,  # 记录原始行号（1-based）
                }
            )
            idx += 1

        return plan, idx, None

    def _execute_plan(
        self, plan: list[dict[str, Any]], context: dict[str, Any]
    ) -> list[list[Any]]:
        """执行计划（不含行映射）"""
        output, _ = self._execute_plan_with_mapping(plan, context, [])
        return output

    def _execute_plan_with_mapping(
        self, plan: list[dict[str, Any]], context: dict[str, Any], original_values: list
    ) -> tuple[list[list[Any]], list[int]]:
        """
        执行渲染计划并返回行映射关系

        参数:
            plan: 渲染计划
            context: 模板上下文
            original_values: 原始行数据

        返回:
            (渲染后的行列表, 行映射列表)
            行映射列表中每个元素表示该渲染行对应的原始模板行号（1-based）
        """
        output: list[list[Any]] = []
        row_mapping: list[int] = []

        for entry in plan:
            entry_type = entry["type"]

            if entry_type == "row":
                rendered_row = []
                for cell in entry["cells"]:
                    if isinstance(cell, str) and self._has_template_syntax(cell):
                        # 先处理 {% img %} 语法
                        processed_cell = self._process_img_syntax(cell, context)
                        rendered: Any = self._render_template_text(
                            processed_cell, context
                        )
                        if isinstance(rendered, str):
                            numeric = safe_convert_to_number(rendered)
                            if numeric is not None and str(numeric) == rendered:
                                rendered = numeric
                        rendered_row.append(rendered)
                    else:
                        rendered_row.append(cell)
                output.append(rendered_row)
                # 记录这一行对应的原始行号
                row_mapping.append(entry.get("source_row", 1))

            elif entry_type == "loop":
                iterable = self._evaluate_expression(entry["expr"], context)
                if iterable is None:
                    continue
                loop_body_source_row = entry.get("body_source_row", 1)
                for item in iterable:
                    loop_context = dict(context)
                    loop_context[entry["var"]] = item
                    loop_context["loop"] = {
                        "index": len([i for i in iterable[: iterable.index(item)]]) + 1
                    }
                    loop_output, loop_mapping = self._execute_plan_with_mapping(
                        entry["body"], loop_context, original_values
                    )
                    output.extend(loop_output)
                    # 循环体的映射都指向循环体模板行
                    row_mapping.extend([loop_body_source_row] * len(loop_output))

            elif entry_type == "if":
                condition = self._evaluate_expression(entry["expr"], context)
                branch = entry["true"] if condition else entry["false"]
                if_output, if_mapping = self._execute_plan_with_mapping(
                    branch, context, original_values
                )
                output.extend(if_output)
                row_mapping.extend(if_mapping)

        return output, row_mapping

    def _evaluate_expression(self, expression: str, context: dict[str, Any]) -> Any:
        if not self._template_environment:
            self._template_environment = self._create_environment()

        try:
            compiled = self._template_environment.compile_expression(expression)
            return compiled(**context)
        except Exception as exc:
            raise TemplateError(
                f"Failed to evaluate expression '{expression}': {exc}"
            ) from exc

    def _extract_marker(self, row: Iterable[Any]) -> str | None:
        for cell in row:
            if isinstance(cell, str):
                trimmed = cell.strip()
                if trimmed.startswith("{%") and trimmed.endswith("%}"):
                    # {% img %} 不是控制语句marker，应该作为普通模板内容处理
                    if trimmed.startswith("{% img"):
                        continue
                    return trimmed
        return None

    def _apply_allowed_filters(self, environment: SandboxedEnvironment) -> None:
        allowed = {}
        for name in self.ALLOWED_FILTERS:
            if name in environment.filters:
                allowed[name] = environment.filters[name]
        environment.filters = allowed

    def _save_merged_cells(self, worksheet: Worksheet) -> list:
        """
        保存合并单元格信息

        参数:
            worksheet: 工作表对象

        返回:
            合并单元格列表，格式: [(min_row, max_row, min_col, max_col), ...]
        """
        ws = worksheet._worksheet
        merged_cells = []
        for merged_range in ws.merged_cells.ranges:
            merged_cells.append(
                (
                    merged_range.min_row,
                    merged_range.max_row,
                    merged_range.min_col,
                    merged_range.max_col,
                )
            )
        return merged_cells

    def _restore_merged_cells(
        self, worksheet: Worksheet, original_merges: list, row_mapping: list[int]
    ) -> None:
        """
        根据行映射关系恢复合并单元格

        参数:
            worksheet: 工作表对象
            original_merges: 原始合并单元格列表
            row_mapping: 渲染行到原始行的映射列表
        """
        ws = worksheet._worksheet

        # 构建反向映射：原始行号 -> 渲染后的行号列表
        reverse_mapping = {}
        for rendered_idx, original_row in enumerate(row_mapping):
            if original_row not in reverse_mapping:
                reverse_mapping[original_row] = []
            reverse_mapping[original_row].append(rendered_idx + 1)  # 1-based

        for min_row, max_row, min_col, max_col in original_merges:
            # 找到原始合并范围内的所有行对应的渲染行
            rendered_rows = []
            for original_row in range(min_row, max_row + 1):
                if original_row in reverse_mapping:
                    rendered_rows.extend(reverse_mapping[original_row])

            if not rendered_rows:
                continue

            # 如果原始是单行合并，在每个渲染行都应用相同的合并
            if min_row == max_row:
                for rendered_row in rendered_rows:
                    if min_col < max_col:  # 只有列合并才需要处理
                        try:
                            ws.merge_cells(
                                start_row=rendered_row,
                                end_row=rendered_row,
                                start_column=min_col,
                                end_column=max_col,
                            )
                        except Exception:
                            pass  # 忽略合并失败
            else:
                # 多行合并：检查原始行映射是否完整
                original_rows_in_merge = set(range(min_row, max_row + 1))
                mapped_original_rows = set()
                for original_row in original_rows_in_merge:
                    if original_row in reverse_mapping:
                        mapped_original_rows.add(original_row)

                # 如果原始合并涉及的行没有全部出现在映射中，跳过此合并
                if mapped_original_rows != original_rows_in_merge:
                    continue

                # 检查渲染后的行是否连续
                rendered_rows_sorted = sorted(set(rendered_rows))

                # 检查是否连续
                is_continuous = True
                for i in range(1, len(rendered_rows_sorted)):
                    if rendered_rows_sorted[i] != rendered_rows_sorted[i - 1] + 1:
                        is_continuous = False
                        break

                # 只有连续且行数匹配时才应用合并
                if is_continuous and len(rendered_rows_sorted) == (
                    max_row - min_row + 1
                ):
                    try:
                        ws.merge_cells(
                            start_row=rendered_rows_sorted[0],
                            end_row=rendered_rows_sorted[-1],
                            start_column=min_col,
                            end_column=max_col,
                        )
                    except Exception:
                        # 忽略合并失败（通常是因为已经合并）
                        pass

    def _save_worksheet_images(self, worksheet: Worksheet) -> list:
        """
        保存工作表中的所有图片信息

        参数:
            worksheet: 工作表对象

        返回:
            图片信息列表
        """
        from copy import deepcopy

        ws = worksheet._worksheet
        saved_images = []

        for img in ws._images:  # type: ignore[attr-defined]
            saved_images.append(deepcopy(img))

        return saved_images

    def _restore_worksheet_images(
        self, worksheet: Worksheet, original_images: list, row_mapping: list[int]
    ) -> None:
        """
        根据行映射关系恢复图片

        参数:
            worksheet: 工作表对象
            original_images: 原始图片列表
            row_mapping: 渲染行到原始行的映射列表
        """
        from openpyxl.drawing.spreadsheet_drawing import TwoCellAnchor, OneCellAnchor
        from openpyxl.utils import coordinate_to_tuple, get_column_letter
        from copy import deepcopy

        ws = worksheet._worksheet

        # 构建反向映射：原始行号 -> 渲染后的行号列表
        reverse_mapping = {}
        for rendered_idx, original_row in enumerate(row_mapping):
            if original_row not in reverse_mapping:
                reverse_mapping[original_row] = []
            reverse_mapping[original_row].append(rendered_idx + 1)  # 1-based

        for img in original_images:
            img_copy = deepcopy(img)
            anchor = img_copy.anchor

            if isinstance(anchor, str):
                row, col = coordinate_to_tuple(anchor.upper())
                if row in reverse_mapping and reverse_mapping[row]:
                    new_row = reverse_mapping[row][0]
                    new_anchor = f"{get_column_letter(col)}{new_row}"
                    img_copy.anchor = new_anchor
                    ws._images.append(img_copy)  # type: ignore[attr-defined]
            elif isinstance(anchor, (TwoCellAnchor, OneCellAnchor)):
                from_row = anchor._from.row + 1  # 0-based to 1-based
                if from_row in reverse_mapping and reverse_mapping[from_row]:
                    new_row = reverse_mapping[from_row][0]
                    row_offset = new_row - from_row

                    anchor._from.row = new_row - 1  # back to 0-based

                    if isinstance(anchor, TwoCellAnchor):
                        to_row = anchor.to.row + 1
                        if to_row in reverse_mapping and reverse_mapping[to_row]:
                            anchor.to.row = reverse_mapping[to_row][0] - 1
                        else:
                            anchor.to.row = anchor.to.row + row_offset

                    ws._images.append(img_copy)  # type: ignore[attr-defined]

    def _process_img_syntax(self, text: str, context: dict[str, Any]) -> str:
        """
        处理图片插入语法，支持多种参数配置

        支持的语法:
        - {% img path %} - 按原始尺寸插入图片
        - {% img path fit %} - 图片适应单元格大小（默认嵌入单元格）
        - {% img path original %} - 图片保持原始大小并嵌入单元格
        - {% img path, width=80, height=40 %} - 指定宽高
        - {% img path fit, width=100 %} - 适应单元格并指定最大宽度
        - {% img path fit, position=top-left, margin=5 %} - 指定位置和边距
        - {% img path original, anchor_to_cell=true %} - 保持原始大小并指定锚定选项

        支持的参数:
        - fit: 启用单元格适应模式
        - original: 保持图片原始大小
        - width/height: 图片尺寸（像素）
        - max_width/max_height: 最大尺寸限制（像素）
        - position: 图片位置 (center/top-left/top-right/bottom-left/bottom-right)
        - margin: 边距（像素）
        - anchor_to_cell: 是否锚定到单元格 (true/false)
        - move_with_cells: 是否随单元格移动 (true/false)
        - resize_with_cells: 是否随单元格调整大小 (true/false)
        - maintain_aspect_ratio: 是否保持宽高比 (true/false)

        参数:
            text: 包含 {% img %} 标记的文本
            context: 模板上下文

        返回:
            替换后的文本
        """
        if not isinstance(text, str) or "{% img" not in text:
            return text

        # 更健壮的匹配模式，处理各种情况
        # 匹配 {% img variable_name [options] %}
        pattern = r"{%\s*img\s+([^\s,]+)(?:\s+(.+?))?\s*%}"

        def replacer(match):
            var_name = match.group(1).strip()
            options_str = match.group(2)

            # 从上下文获取图片路径
            image_path = context.get(var_name)
            if not image_path:
                return ""

            # 解析选项和参数
            kwargs_list = []

            if options_str:
                # 分离关键字和参数
                parts = [part.strip() for part in options_str.split(",")]

                # 检查模式关键字
                if "fit" in parts:
                    parts.remove("fit")
                    kwargs_list.append("fit_cell=True")

                if "original" in parts:
                    parts.remove("original")
                    kwargs_list.append("keep_original_size=True")

                # 处理其他参数
                for part in parts:
                    if "=" in part:
                        key, value = part.split("=", 1)
                        key = key.strip()
                        value = value.strip()

                        # 处理布尔值
                        if value.lower() in ("true", "false"):
                            kwargs_list.append(f"{key}={value.capitalize()}")
                        # 处理数字
                        elif value.isdigit():
                            kwargs_list.append(f"{key}={value}")
                        # 处理字符串（需要加引号）
                        else:
                            kwargs_list.append(f"{key}='{value}'")

            # 如果只有关键字
            elif options_str == "fit":
                kwargs_list.append("fit_cell=True")
            elif options_str == "original":
                kwargs_list.append("keep_original_size=True")

            # 构造 insert_image 调用
            if kwargs_list:
                return (
                    f"{{{{ insert_image('{image_path}', {', '.join(kwargs_list)}) }}}}"
                )
            else:
                return f"{{{{ insert_image('{image_path}') }}}}"

        return re.sub(pattern, replacer, text)

    def _copy_row_styles(self, worksheet, source_row: int, target_row: int) -> None:
        """
        复制行样式从源行到目标行

        参数:
            worksheet: 工作表对象
            source_row: 源行号 (1-based)
            target_row: 目标行号 (1-based)
        """
        try:
            # 获取源行和目标行的openpyxl对象
            source_ws = worksheet._worksheet
            target_ws = worksheet._worksheet

            # 复制行高
            if source_row in source_ws.row_dimensions:
                source_height = source_ws.row_dimensions[source_row].height
                if source_height:
                    target_ws.row_dimensions[target_row].height = source_height

            # 复制单元格样式
            max_col = worksheet.max_column
            for col in range(1, max_col + 1):
                source_cell = source_ws.cell(row=source_row, column=col)
                target_cell = target_ws.cell(row=target_row, column=col)

                # 复制单元格样式
                if source_cell.has_style:
                    # 复制字体
                    if source_cell.font:
                        target_cell.font = source_cell.font
                    # 复制填充
                    if source_cell.fill:
                        target_cell.fill = source_cell.fill
                    # 复制边框
                    if source_cell.border:
                        target_cell.border = source_cell.border
                    # 复制对齐
                    if source_cell.alignment:
                        target_cell.alignment = source_cell.alignment
                    # 复制数字格式
                    if source_cell.number_format:
                        target_cell.number_format = source_cell.number_format

            # 复制列宽（如果有特定设置）
            for col in range(1, max_col + 1):
                col_letter = self._get_column_letter(col)
                if col_letter in source_ws.column_dimensions:
                    source_width = source_ws.column_dimensions[col_letter].width
                    if source_width:
                        target_ws.column_dimensions[col_letter].width = source_width

        except Exception as e:
            # 如果样式复制失败，记录警告但不中断流程
            import logging

            logger = logging.getLogger(__name__)
            logger.warning(f"复制行样式失败: {e}")

    def _ensure_safe_context(self, context: dict[str, Any]) -> None:
        def _validate(value: Any) -> None:
            if isinstance(value, (str, int, float, bool)) or value is None:
                return
            if isinstance(value, Mapping):
                for key, inner in value.items():
                    if isinstance(key, str) and key.startswith("__"):
                        raise TemplateError(f"Disallowed context key: {key}")
                    _validate(inner)
                return
            if isinstance(value, (set, frozenset)):
                for item in value:
                    _validate(item)
                return
            if isinstance(value, Sequence) and not isinstance(
                value, (str, bytes, bytearray)
            ):
                for item in value:
                    _validate(item)
                return
            raise TemplateError(
                f"Unsupported context value type: {type(value).__name__}"
            )

        if not isinstance(context, Mapping):
            raise TemplateError("Context must be a mapping")

        for key, value in context.items():
            if isinstance(key, str) and key.startswith("__"):
                raise TemplateError(f"Disallowed context key: {key}")
            if callable(value):
                raise TemplateError(f"Disallowed callable in context for key '{key}'")
            _validate(value)
