"""`workflow` 共享输出资源: `sheetbook` 实现(内部模块).

说明:
- 承载 `sheetbook_sheet`/`sheetbook_append` 的内存物化、读取与导出
- 运行时需兼容 `Python 3.6`
"""

from abc import ABC
from contextlib import suppress
from pathlib import Path
from typing import Any, Dict, FrozenSet, Iterator, List, Optional, Tuple, cast

from .._internal.utils.excel import escape_excel_formula
from ..events import EVENT_DIAGNOSTIC_WARNING
from ..events._events import DiagnosticWarningEvent
from ..sinks._internal.base import create_temp_path
from ..vendor.compact.typing_extensionsx import override
from ..vendor.dataclassesx import dataclass
from .resources_base import ScalimWorkflowWriteError, WorkflowResourceManagerBase, acquire_write_lock, release_write_lock
from .resources_csv import WorkflowCsvInput, build_alignment_mapping, describe_header_diff, iter_csv_rows, read_csv_header
from .resources_workbook import best_effort_close_write_only_workbook_worksheets, get_openpyxl_workbook_class

# 内部实现仍沿用原有局部命名,减少重构噪音.
_build_alignment_mapping = build_alignment_mapping
_describe_header_diff = describe_header_diff
_iter_csv_rows = iter_csv_rows
_read_csv_header = read_csv_header
_best_effort_close_write_only_workbook_worksheets = best_effort_close_write_only_workbook_worksheets
_get_openpyxl_workbook_class = get_openpyxl_workbook_class


def _materialize_aligned_csv_rows(expected: List[str], mapping: List[int], *, input_csv: WorkflowCsvInput) -> List[List[str]]:
    _ = list(expected)
    rows: List[List[str]] = []
    for row in _iter_csv_rows(input_csv):
        out_row: List[str] = []
        for src_idx in mapping:
            out_row.append(row[src_idx] if src_idx >= 0 and src_idx < len(row) else "")
        rows.append(out_row)
    return rows


def _sorted_sheetbook_segments(segments: List["_SheetBookSegment"]) -> List["_SheetBookSegment"]:
    return sorted(segments, key=lambda seg: (int(seg.decl_order), str(seg.producer_node_id)))


def _write_sheetbook_plan_to_openpyxl_workbook(workbook: object, plan: "_SheetBookPlan") -> None:
    wb = cast("Any", workbook)  # pragma: allow-cast openpyxl workbook runtime boundary
    for sheet_name in plan.sheet_order:
        sheet_plan = plan.sheets.get(sheet_name)
        if sheet_plan is None:
            continue  # pragma: no cover  # pragma: allow-no-cover unreachable: sheet_plan always exists
        ws = wb.create_sheet(str(sheet_name))
        header_written = False
        fields = list(sheet_plan.baseline_header)
        escaped_fields = [escape_excel_formula(x, allow_formulas=bool(plan.export_allow_formulas)) for x in fields]
        for seg in _sorted_sheetbook_segments(sheet_plan.segments):
            if seg.header_policy == "always" or (seg.header_policy == "once" and not header_written):
                _ = ws.append(list(escaped_fields))
                header_written = True
            # `never`: 不输出表头

            for row in seg.rows:
                out_row = [escape_excel_formula(v, allow_formulas=bool(plan.export_allow_formulas)) for v in row]
                _ = ws.append(out_row)


def _save_openpyxl_workbook_atomic(workbook: object, *, output_path: str) -> None:
    wb = cast("Any", workbook)  # pragma: allow-cast openpyxl workbook runtime boundary
    temp_path = create_temp_path(output_path, ".xlsx.tmp")
    temp_obj = Path(temp_path)
    try:
        wb.save(temp_obj)
        _ = temp_obj.replace(output_path)
    except Exception as exc:
        with suppress(Exception):
            temp_obj.unlink()
        msg = "Sheetbook export failed: {}: {}".format(type(exc).__name__, exc)
        raise ScalimWorkflowWriteError(msg) from exc


@dataclass(frozen=True)
class SheetBookDef:
    resource_id: str
    budget_max_sheets: int
    budget_max_total_cells: int
    export_path: Optional[str]
    export_write_lock: bool
    export_allow_formulas: bool = False


@dataclass
class _SheetBookSegment:
    producer_node_id: str
    decl_order: int
    rows: List[List[str]]
    header_policy: str


@dataclass
class _SheetBookSheetPlan:
    sheet: str
    baseline_header: List[str]
    segments: List[_SheetBookSegment]
    cell_count: int = 0


@dataclass
class _SheetBookPlan:
    resource_id: str
    budget_max_sheets: int
    budget_max_total_cells: int
    export_path: Optional[str]
    export_write_lock: bool
    export_allow_formulas: bool
    sheet_decl_order: Dict[str, int]
    sheet_order: List[str]
    sheets: Dict[str, _SheetBookSheetPlan]
    lock_path: Optional[Path] = None
    total_cells: int = 0
    last_workflow_node_id: Optional[str] = None


class _WorkflowSheetBookResourceMixin(WorkflowResourceManagerBase, ABC):
    @staticmethod
    def _sheetbook_has_alignment_mismatch(expected: List[str], input_header: List[str], *, align_by: str) -> bool:
        if str(align_by) == "field_id":
            exp_set = {str(x) for x in expected}
            act_set = {str(x) for x in input_header}
            missing = exp_set.difference(act_set)
            extra = act_set.difference(exp_set)
            return bool(missing or extra)
        return list(input_header) != list(expected)

    def _emit_sheetbook_commit(self, plan: _SheetBookPlan, *, display_path: str) -> None:
        node_id = plan.last_workflow_node_id or "__wf__commit"
        self._emit_resource_commit(
            workflow_node_id=node_id, resource_type="sheetbook", resource_id=plan.resource_id, path=str(display_path)
        )

    def _maybe_acquire_sheetbook_write_lock(self, plan: _SheetBookPlan, *, output_path: str) -> None:
        if not bool(plan.export_write_lock):
            return
        lock_path = acquire_write_lock(
            output_path,
            owner={
                "workflow_exec_id": self._workflow_exec_id,
                "resource_type": "sheetbook",
                "resource_id": str(plan.resource_id),
            },
        )
        plan.lock_path = lock_path

    def _release_sheetbook_write_lock(self, plan: _SheetBookPlan) -> None:
        if plan.lock_path is None:
            return
        release_write_lock(plan.lock_path)
        plan.lock_path = None

    def _sheetbook_sheet_prepare_action(
        self,
        plan: _SheetBookPlan,
        *,
        workflow_node_id: str,
        sheetbook_id: str,
        sheet_name: str,
        on_conflict: str,
    ) -> Tuple[str, bool]:
        action = "write"
        pending_skip = False
        with self._lock:
            existing = plan.sheets.get(sheet_name)
            if existing is not None:
                if on_conflict == "skip":
                    action = "skip"
                    plan.last_workflow_node_id = str(workflow_node_id)
                    pending_skip = True
                if on_conflict == "error":
                    msg = "Sheet conflict (sheetbook_sheet): sheetbook={!r}, sheet={!r}".format(str(sheetbook_id), sheet_name)
                    raise ScalimWorkflowWriteError(msg, diff=["on_conflict=error", "existing_sheet=present"])
                if on_conflict == "overwrite":
                    action = "overwrite"
            elif len(plan.sheets) >= int(plan.budget_max_sheets):
                msg = "Sheetbook budget exceeded: max_sheets (sheetbook={!r})".format(str(sheetbook_id))
                diff = [
                    "budget.max_sheets={}".format(int(plan.budget_max_sheets)),
                    "current_sheets={}".format(len(plan.sheets)),
                    "new_sheet={!r}".format(sheet_name),
                ]
                raise ScalimWorkflowWriteError(msg, diff=diff)
        return action, pending_skip

    def _sheetbook_sheet_store(
        self,
        plan: _SheetBookPlan,
        *,
        workflow_node_id: str,
        sheet_name: str,
        decl_order: int,
        input_node_id: str,
        expected: List[str],
        rows: List[List[str]],
        new_sheet_cells: int,
    ) -> None:
        with self._lock:
            existing = plan.sheets.get(sheet_name)
            old_cells = 0
            if existing is not None:
                old_cells = int(existing.cell_count)

            new_total = int(plan.total_cells) - int(old_cells) + int(new_sheet_cells)
            self._check_sheetbook_budget(plan, new_total_cells=new_total)

            existing_decl_order = plan.sheet_decl_order.get(sheet_name)
            resolved_decl_order = int(decl_order)
            if existing_decl_order is None or resolved_decl_order < int(existing_decl_order):
                plan.sheet_decl_order[sheet_name] = resolved_decl_order
            plan.sheet_order = sorted(plan.sheet_decl_order.keys(), key=lambda name: (plan.sheet_decl_order.get(name, 0), str(name)))

            sheet_plan = _SheetBookSheetPlan(
                sheet=sheet_name,
                baseline_header=list(expected),
                segments=[],
                cell_count=int(new_sheet_cells),
            )
            sheet_plan.segments.append(
                _SheetBookSegment(
                    producer_node_id=str(input_node_id),
                    decl_order=int(decl_order),
                    rows=rows,
                    header_policy="once",
                )
            )
            plan.sheets[sheet_name] = sheet_plan
            plan.total_cells = int(new_total)
            plan.last_workflow_node_id = str(workflow_node_id)

    def _sheetbook_append_prepare(  # noqa: C901
        self,
        plan: _SheetBookPlan,
        *,
        workflow_node_id: str,
        sheetbook_id: str,
        sheet_name: str,
        decl_order: int,
        input_node_id: str,
        input_header: List[str],
        align_by: str,
        on_mismatch: str,
    ) -> Tuple[List[str], List[int], Optional[DiagnosticWarningEvent], Optional[Dict[str, object]], bool]:
        msg: str
        pending_warning: Optional[DiagnosticWarningEvent] = None
        pending_warning_meta: Optional[Dict[str, object]] = None
        pending_skip = False

        with self._lock:
            sheet_plan = plan.sheets.get(sheet_name)
            if sheet_plan is None:
                if len(plan.sheets) >= int(plan.budget_max_sheets):
                    msg = "Sheetbook budget exceeded: max_sheets (sheetbook={!r})".format(str(sheetbook_id))
                    diff = [
                        "budget.max_sheets={}".format(int(plan.budget_max_sheets)),
                        "current_sheets={}".format(len(plan.sheets)),
                        "new_sheet={!r}".format(sheet_name),
                    ]
                    raise ScalimWorkflowWriteError(msg, diff=diff)
                plan.sheet_decl_order[sheet_name] = int(decl_order)
                plan.sheet_order = sorted(plan.sheet_decl_order.keys(), key=lambda name: (plan.sheet_decl_order.get(name, 0), str(name)))
                sheet_plan = _SheetBookSheetPlan(
                    sheet=sheet_name,
                    baseline_header=list(input_header),
                    segments=[],
                    cell_count=0,
                )
                plan.sheets[sheet_name] = sheet_plan
            else:
                existing_decl_order = plan.sheet_decl_order.get(sheet_name)
                resolved_decl_order = int(decl_order)
                if existing_decl_order is None or resolved_decl_order < int(existing_decl_order):
                    plan.sheet_decl_order[sheet_name] = resolved_decl_order
                    plan.sheet_order = sorted(
                        plan.sheet_decl_order.keys(),
                        key=lambda name: (plan.sheet_decl_order.get(name, 0), str(name)),
                    )

            expected = list(sheet_plan.baseline_header)
            mapping = _build_alignment_mapping(expected, input_header)

            if self._sheetbook_has_alignment_mismatch(expected, input_header, align_by=str(align_by)):
                diff = _describe_header_diff(expected, input_header)
                if on_mismatch == "error":
                    msg = "Field alignment mismatch (sheetbook_append): sheetbook={!r}, sheet={!r}".format(str(sheetbook_id), sheet_name)
                    raise ScalimWorkflowWriteError(msg, diff=diff)
                if on_mismatch == "warn":
                    pending_warning = DiagnosticWarningEvent(
                        message="Field alignment mismatch (warn): sheetbook={!r}, sheet={!r}".format(str(sheetbook_id), sheet_name),
                        source_id=None,
                        field_id=None,
                        lookup_key={"expected": expected, "actual": list(input_header)},
                        row_id=None,
                    )
                    pending_warning_meta = {"workflow_exec_id": self._workflow_exec_id, "workflow_node_id": str(workflow_node_id)}
                if on_mismatch == "skip":
                    plan.last_workflow_node_id = str(workflow_node_id)
                    pending_skip = True

            if not pending_skip:
                # 重复写入检测: 同一生产者不应写入同一工作表多次.
                for seg in sheet_plan.segments:
                    if str(seg.producer_node_id) == str(input_node_id):
                        msg = "Duplicate sheetbook write for the same producer: sheetbook={!r}, sheet={!r}, producer={!r}".format(
                            str(sheetbook_id),
                            sheet_name,
                            str(input_node_id),
                        )
                        raise ScalimWorkflowWriteError(msg, diff=["producer_node_id={!r}".format(str(input_node_id))])
        return expected, mapping, pending_warning, pending_warning_meta, pending_skip

    def _sheetbook_append_apply(
        self,
        plan: _SheetBookPlan,
        *,
        sheetbook_id: str,
        sheet_name: str,
        append_cells: int,
        workflow_node_id: str,
        input_node_id: str,
        decl_order: int,
        rows: List[List[str]],
        header_policy: str,
    ) -> None:
        with self._lock:
            sheet_plan = plan.sheets.get(sheet_name)
            if sheet_plan is None:  # pragma: no cover  # pragma: allow-no-cover unreachable: sheet_plan always exists
                msg = "Sheetbook sheet missing during append: sheetbook={!r}, sheet={!r}".format(str(sheetbook_id), sheet_name)
                raise ScalimWorkflowWriteError(msg)

            new_total = int(plan.total_cells) + int(append_cells)
            self._check_sheetbook_budget(plan, new_total_cells=new_total)
            sheet_plan.segments.append(
                _SheetBookSegment(
                    producer_node_id=str(input_node_id),
                    decl_order=int(decl_order),
                    rows=rows,
                    header_policy=str(header_policy),
                )
            )
            sheet_plan.cell_count = int(sheet_plan.cell_count) + int(append_cells)
            plan.total_cells = int(new_total)
            plan.last_workflow_node_id = str(workflow_node_id)

    def _get_or_create_sheetbook(self, sheetbook_id: str, *, workflow_node_id: str) -> _SheetBookPlan:
        key = str(sheetbook_id)
        with self._lock:
            existing = cast("Optional[_SheetBookPlan]", self._sheetbooks.get(key))  # pragma: allow-cast sheetbook plan typed narrowing
            if existing is not None:
                return existing

            raw_def = self._sheetbook_defs.get(key)
            if raw_def is None:
                msg = "Unknown sheetbook resource id: {!r}".format(key)
                raise ScalimWorkflowWriteError(msg)

            raw_def = cast("SheetBookDef", raw_def)  # pragma: allow-cast sheetbook def typed narrowing
            plan = SheetBookPlan(
                resource_id=str(raw_def.resource_id),
                budget_max_sheets=int(raw_def.budget_max_sheets),
                budget_max_total_cells=int(raw_def.budget_max_total_cells),
                export_path=str(raw_def.export_path) if raw_def.export_path is not None else None,
                export_write_lock=bool(raw_def.export_write_lock),
                export_allow_formulas=bool(raw_def.export_allow_formulas),
                sheet_decl_order={},
                sheet_order=[],
                sheets={},
            )
            self._sheetbooks[key] = plan

        display_path = plan.export_path if plan.export_path is not None else "<memory>"
        self._emit_resource_create(
            workflow_node_id=str(workflow_node_id),
            resource_type="sheetbook",
            resource_id=str(key),
            path=str(display_path),
        )
        return plan

    def _check_sheetbook_budget(
        self,
        plan: _SheetBookPlan,
        *,
        new_total_cells: int,
        allow_over_budget: bool = False,
    ) -> None:
        if allow_over_budget:  # pragma: no cover  # pragma: allow-no-cover test-only budget bypass
            return  # pragma: no cover  # pragma: allow-no-cover test-only budget bypass
        limit = int(plan.budget_max_total_cells)
        if int(new_total_cells) <= limit:
            return
        diff = [
            "budget.max_total_cells={}".format(limit),
            "new_total_cells={}".format(int(new_total_cells)),
        ]
        msg = "Sheetbook budget exceeded: sheetbook={!r}".format(str(plan.resource_id))
        raise ScalimWorkflowWriteError(msg, diff=diff)

    def apply_sheetbook_sheet(
        self,
        *,
        workflow_node_id: str,
        decl_order: int,
        sheetbook_id: str,
        sheet: str,
        input_node_id: str,
        input_output_id: str,
        input_csv: WorkflowCsvInput,
        on_conflict: str,
    ) -> None:
        plan = self._get_or_create_sheetbook(sheetbook_id, workflow_node_id=str(workflow_node_id))
        sheet_name = str(sheet)
        input_header = _read_csv_header(input_csv)
        action, pending_skip = self._sheetbook_sheet_prepare_action(
            plan,
            workflow_node_id=str(workflow_node_id),
            sheetbook_id=str(sheetbook_id),
            sheet_name=sheet_name,
            on_conflict=str(on_conflict),
        )

        if pending_skip:
            display_path = plan.export_path if plan.export_path is not None else "<memory>"
            self._emit_resource_write(
                workflow_node_id=str(workflow_node_id),
                resource_type="sheetbook",
                resource_id=str(sheetbook_id),
                path=str(display_path),
                write_kind="sheetbook_sheet",
                action=action,
                input_node_id=str(input_node_id),
                input_output_id=str(input_output_id),
                sheet=sheet_name,
            )
            return

        expected = list(input_header)
        mapping = _build_alignment_mapping(expected, input_header)

        rows = _materialize_aligned_csv_rows(expected, mapping, input_csv=input_csv)
        row_count = len(rows)
        new_sheet_cells = int(row_count) * len(expected)

        self._sheetbook_sheet_store(
            plan,
            workflow_node_id=str(workflow_node_id),
            sheet_name=sheet_name,
            decl_order=int(decl_order),
            input_node_id=str(input_node_id),
            expected=expected,
            rows=rows,
            new_sheet_cells=int(new_sheet_cells),
        )

        display_path = plan.export_path if plan.export_path is not None else "<memory>"
        self._emit_resource_write(
            workflow_node_id=str(workflow_node_id),
            resource_type="sheetbook",
            resource_id=str(sheetbook_id),
            path=str(display_path),
            write_kind="sheetbook_sheet",
            action=action,
            input_node_id=str(input_node_id),
            input_output_id=str(input_output_id),
            sheet=sheet_name,
        )

    def apply_sheetbook_append(
        self,
        *,
        workflow_node_id: str,
        decl_order: int,
        sheetbook_id: str,
        sheet: str,
        input_node_id: str,
        input_output_id: str,
        input_csv: WorkflowCsvInput,
        align_by: str,
        header_policy: str,
        on_mismatch: str,
    ) -> None:
        plan = self._get_or_create_sheetbook(sheetbook_id, workflow_node_id=str(workflow_node_id))
        sheet_name = str(sheet)
        input_header = _read_csv_header(input_csv)
        expected, mapping, pending_warning, pending_warning_meta, pending_skip = self._sheetbook_append_prepare(
            plan,
            workflow_node_id=str(workflow_node_id),
            sheetbook_id=str(sheetbook_id),
            sheet_name=sheet_name,
            decl_order=int(decl_order),
            input_node_id=str(input_node_id),
            input_header=list(input_header),
            align_by=str(align_by),
            on_mismatch=str(on_mismatch),
        )

        if pending_warning is not None:
            _ = self._instrumentation.emit(EVENT_DIAGNOSTIC_WARNING, pending_warning, meta=pending_warning_meta)

        if pending_skip:
            display_path = plan.export_path if plan.export_path is not None else "<memory>"
            self._emit_resource_write(
                workflow_node_id=str(workflow_node_id),
                resource_type="sheetbook",
                resource_id=str(sheetbook_id),
                path=str(display_path),
                write_kind="sheetbook_append",
                action="skip",
                input_node_id=str(input_node_id),
                input_output_id=str(input_output_id),
                sheet=sheet_name,
            )
            return

        rows = _materialize_aligned_csv_rows(expected, mapping, input_csv=input_csv)

        append_rows = len(rows)
        append_cells = int(append_rows) * len(expected)

        self._sheetbook_append_apply(
            plan,
            sheetbook_id=str(sheetbook_id),
            sheet_name=sheet_name,
            append_cells=int(append_cells),
            workflow_node_id=str(workflow_node_id),
            input_node_id=str(input_node_id),
            decl_order=int(decl_order),
            rows=rows,
            header_policy=str(header_policy),
        )

        display_path = plan.export_path if plan.export_path is not None else "<memory>"
        self._emit_resource_write(
            workflow_node_id=str(workflow_node_id),
            resource_type="sheetbook",
            resource_id=str(sheetbook_id),
            path=str(display_path),
            write_kind="sheetbook_append",
            action="append",
            input_node_id=str(input_node_id),
            input_output_id=str(input_output_id),
            sheet=sheet_name,
        )

    def iter_sheetbook_sheet_rows(  # noqa: C901
        self,
        *,
        consumer_node_id: str,
        visible_producer_node_ids: FrozenSet[str],
        producer_node_id: str,
        sheetbook_id: str,
        sheet: str,
    ) -> Iterator[Dict[str, object]]:
        """读取 `sheetbook` 的行快照(按 `ref.node` 截断;按依赖可见性过滤).

        返回: `Iterator[Dict[str, object]]`, 每行必须为 `JSON-like mapping`.
        """
        _ = str(consumer_node_id)
        producer = str(producer_node_id)
        sb_id = str(sheetbook_id)
        sheet_name = str(sheet)
        visible = frozenset(str(x) for x in visible_producer_node_ids)

        with self._lock:
            plan = cast("Optional[_SheetBookPlan]", self._sheetbooks.get(sb_id))  # pragma: allow-cast sheetbook plan typed narrowing
            if plan is None:
                msg = "Unknown sheetbook resource id: {!r}".format(sb_id)
                raise ValueError(msg)
            sheet_plan = plan.sheets.get(sheet_name)
            if sheet_plan is None:
                msg = "Unknown sheetbook sheet: sheetbook={!r}, sheet={!r}".format(sb_id, sheet_name)
                raise ValueError(msg)

            cutoff_idx = None
            ordered_segments = _sorted_sheetbook_segments(list(sheet_plan.segments))
            for idx, seg in enumerate(ordered_segments):
                if str(seg.producer_node_id) == producer:
                    cutoff_idx = int(idx)
                    break
            if cutoff_idx is None:
                msg = "Unknown sheetbook ref node for sheet: node={!r}, sheetbook={!r}, sheet={!r}".format(producer, sb_id, sheet_name)
                raise ValueError(msg)

            baseline_header = list(sheet_plan.baseline_header)
            segments: List[Tuple[str, List[List[str]]]] = []
            for seg in ordered_segments[: cutoff_idx + 1]:
                seg_producer = str(seg.producer_node_id)
                if seg_producer != producer and seg_producer not in visible:
                    continue
                segments.append((seg_producer, list(seg.rows)))

        def _iter() -> Iterator[Dict[str, object]]:
            for _seg_producer, seg_rows in segments:
                for row_values in seg_rows:
                    row: Dict[str, object] = {}
                    for idx, key in enumerate(baseline_header):
                        row[str(key)] = row_values[idx] if idx >= 0 and idx < len(row_values) else ""
                    yield row

        return _iter()

    @override
    def _commit_sheetbook(self, plan: object) -> None:
        p = cast("_SheetBookPlan", plan)  # pragma: allow-cast sheetbook plan typed narrowing
        export_path = p.export_path
        display_path = export_path if export_path is not None else "<memory>"
        if export_path is None:
            self._emit_sheetbook_commit(p, display_path=str(display_path))
            return

        output_path = str(export_path)
        output_dir = Path(output_path).parent
        output_dir.mkdir(parents=True, exist_ok=True)

        self._maybe_acquire_sheetbook_write_lock(p, output_path=output_path)

        try:
            workbook_cls = _get_openpyxl_workbook_class()
        except ImportError as exc:
            with suppress(Exception):
                self._release_sheetbook_write_lock(p)
            raise ScalimWorkflowWriteError(str(exc)) from exc

        wb = workbook_cls(write_only=True)
        try:
            _write_sheetbook_plan_to_openpyxl_workbook(wb, p)
            _save_openpyxl_workbook_atomic(wb, output_path=output_path)
        except Exception:
            _best_effort_close_write_only_workbook_worksheets(wb)
            raise
        finally:
            with suppress(Exception):
                wb.close()
            with suppress(Exception):
                self._release_sheetbook_write_lock(p)

        self._emit_sheetbook_commit(p, display_path=str(display_path))

    @override
    def _discard_sheetbook(self, plan: object, *, workflow_node_id: str, reason: str) -> None:
        p = cast("_SheetBookPlan", plan)  # pragma: allow-cast sheetbook plan typed narrowing
        with suppress(Exception):
            self._release_sheetbook_write_lock(p)
        node_id = p.last_workflow_node_id or str(workflow_node_id)
        display_path = p.export_path if p.export_path is not None else "<memory>"
        self._emit_resource_discard(
            workflow_node_id=node_id,
            resource_type="sheetbook",
            resource_id=p.resource_id,
            path=str(display_path),
            reason=str(reason),
        )


__all__ = [
    "SheetBookDef",
    "SheetBookPlan",
    "SheetBookSegment",
    "SheetBookSheetPlan",
    "WorkflowSheetBookResourceMixin",
]

SheetBookPlan = _SheetBookPlan
SheetBookSegment = _SheetBookSegment
SheetBookSheetPlan = _SheetBookSheetPlan
WorkflowSheetBookResourceMixin = _WorkflowSheetBookResourceMixin
