"""Fixed Origin adapter for origin.plot@v2.

This process accepts exactly one argument: a Node-created job directory. It never
installs packages, evaluates user code, downloads data, or resolves paths from the
request. terminal.json is its only terminal-state contract.
"""

from __future__ import annotations

import csv
import hashlib
import json
import math
import os
import re
import sys
import zlib
from datetime import datetime, timezone
from importlib.metadata import PackageNotFoundError, version
from itertools import pairwise
from pathlib import Path
from typing import Any

PLOT_CONFIG = {
    "line": ("line", "l"),
    "scatter": ("scatter", "s"),
    "line_scatter": ("linesymb", "y"),
    "column": ("column", "c"),
    "bar": ("bar", 215),
    "grouped_column": ("column", "c"),
    "y_error": ("ERRBAR", "y"),
    "contour": ("TriContour", 243),
    "surface_3d": ("glCMAP", 103),
    "ternary": ("ternary", 245),
    "box": ("box", 206),
    "histogram": ("hist", 219),
    "stacked_column": ("StackColumn", 213),
    "bubble": ("scatter", "s"),
    "band": ("line", "l"),
    "area": ("area", 204),
    "stacked_area": ("stackarea", 214),
    "polar": ("polar", 192),
    "pie": ("pie", 225),
    "stacked_bar": ("bar", 216),
    "stacked_line": ("line", "l"),
    "violin": ("Violin", 206),
}
STACKED_PLOT_TYPES = {"stacked_column", "stacked_area", "stacked_bar"}
OFFSET_STACKED_PLOT_TYPES = {"stacked_line"}
COMPOSITION_PLOT_TYPES = {"multi_panel", "recipe"}
_CUMULATIVE_STACK_COMMAND = "layer -b s 1"
XYZ_PLOT_TYPES = {"contour", "surface_3d", "ternary", "heatmap"}
FORMATS = {"opju", "png", "svg", "pdf"}
LINE_STYLES = {
    "solid": 1,
    "dash": 2,
    "dot": 3,
    "dash_dot": 4,
    "dash_dot_dot": 5,
}
SYMBOLS = {
    "square": 1,
    "circle": 2,
    "triangle_up": 3,
    "diamond": 5,
    "cross": 7,
    "plus": 6,
}
LEGEND_POSITIONS = {
    "top_left": (700, 500),
    "top_right": (6800, 500),
    "bottom_left": (700, 7200),
    "bottom_right": (6800, 7200),
}
PANEL_LAYOUT = {
    (1, 1): {"left": 13, "right": 13, "top": 14, "bottom": 14, "xgap": 0, "ygap": 0},
    (1, 2): {"left": 9, "right": 4, "top": 14, "bottom": 14, "xgap": 11, "ygap": 0},
    (2, 1): {"left": 13, "right": 13, "top": 14, "bottom": 12, "xgap": 0, "ygap": 10},
    (2, 2): {"left": 9, "right": 4, "top": 14, "bottom": 12, "xgap": 11, "ygap": 10},
}
ADAPTER_VERSION = "1.3.0"


def _server_executable(command: str) -> Path:
    value = os.path.expandvars(command.strip())
    match = re.match(r'^\s*"([^"]+\.exe)"|^\s*(.+?\.exe)(?:\s|$)', value, re.IGNORECASE)
    if match is None:
        raise ValueError("Origin COM server executable is invalid")
    return Path(match.group(1) or match.group(2))


def _windows_file_version(path: Path) -> str:
    import ctypes
    from ctypes import wintypes

    class FixedFileInfo(ctypes.Structure):
        _fields_ = [
            ("signature", wintypes.DWORD),
            ("structure_version", wintypes.DWORD),
            ("file_version_ms", wintypes.DWORD),
            ("file_version_ls", wintypes.DWORD),
            ("product_version_ms", wintypes.DWORD),
            ("product_version_ls", wintypes.DWORD),
            ("file_flags_mask", wintypes.DWORD),
            ("file_flags", wintypes.DWORD),
            ("file_os", wintypes.DWORD),
            ("file_type", wintypes.DWORD),
            ("file_subtype", wintypes.DWORD),
            ("file_date_ms", wintypes.DWORD),
            ("file_date_ls", wintypes.DWORD),
        ]

    class LanguageAndCodePage(ctypes.Structure):
        _fields_ = [("language", wintypes.WORD), ("code_page", wintypes.WORD)]

    version_api = ctypes.WinDLL("version", use_last_error=True)
    version_api.GetFileVersionInfoSizeW.argtypes = [
        wintypes.LPCWSTR,
        ctypes.POINTER(wintypes.DWORD),
    ]
    version_api.GetFileVersionInfoSizeW.restype = wintypes.DWORD
    version_api.GetFileVersionInfoW.argtypes = [
        wintypes.LPCWSTR,
        wintypes.DWORD,
        wintypes.DWORD,
        ctypes.c_void_p,
    ]
    version_api.GetFileVersionInfoW.restype = wintypes.BOOL
    version_api.VerQueryValueW.argtypes = [
        ctypes.c_void_p,
        wintypes.LPCWSTR,
        ctypes.POINTER(ctypes.c_void_p),
        ctypes.POINTER(wintypes.UINT),
    ]
    version_api.VerQueryValueW.restype = wintypes.BOOL
    ignored_handle = wintypes.DWORD()
    size = version_api.GetFileVersionInfoSizeW(str(path), ctypes.byref(ignored_handle))
    if size == 0:
        raise ctypes.WinError(ctypes.get_last_error())
    buffer = ctypes.create_string_buffer(size)
    if not version_api.GetFileVersionInfoW(str(path), 0, size, buffer):
        raise ctypes.WinError(ctypes.get_last_error())
    value = ctypes.c_void_p()
    value_size = wintypes.UINT()
    if not version_api.VerQueryValueW(buffer, "\\", ctypes.byref(value), ctypes.byref(value_size)):
        raise ctypes.WinError(ctypes.get_last_error())
    info = ctypes.cast(value, ctypes.POINTER(FixedFileInfo)).contents
    translation = ctypes.c_void_p()
    translation_size = wintypes.UINT()
    if version_api.VerQueryValueW(
        buffer,
        r"\VarFileInfo\Translation",
        ctypes.byref(translation),
        ctypes.byref(translation_size),
    ):
        count = translation_size.value // ctypes.sizeof(LanguageAndCodePage)
        translations = ctypes.cast(
            translation, ctypes.POINTER(LanguageAndCodePage * count)
        ).contents
        for item in translations:
            prefix = rf"\StringFileInfo\{item.language:04x}{item.code_page:04x}"
            strings = {}
            for name in ("FileDescription", "ProductVersion"):
                string_value = ctypes.c_void_p()
                string_size = wintypes.UINT()
                if version_api.VerQueryValueW(
                    buffer,
                    f"{prefix}\\{name}",
                    ctypes.byref(string_value),
                    ctypes.byref(string_size),
                ):
                    strings[name] = ctypes.wstring_at(string_value).strip()
            description = strings.get("FileDescription", "")
            match = re.fullmatch(r"Origin(?:Pro)?\s+(.+)", description)
            if match:
                return match.group(1)
            if strings.get("ProductVersion"):
                return strings["ProductVersion"]
    parts = (
        info.file_version_ms >> 16,
        info.file_version_ms & 0xFFFF,
        info.file_version_ls >> 16,
        info.file_version_ls & 0xFFFF,
    )
    if not any(parts):
        raise ValueError("Origin executable has no file version")
    return ".".join(str(part) for part in parts)


def _registered_origin_version(winreg: Any) -> str | None:
    configured = os.environ.get("ZCBOT_ORIGIN_EXE")
    if configured:
        try:
            executable = Path(configured).expanduser().resolve(strict=True)
            if executable.is_file() and executable.name.lower().startswith("origin"):
                return _windows_file_version(executable)
        except (OSError, ValueError):
            pass
    try:
        with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r"Origin.ApplicationSI\CLSID") as clsid_key:
            clsid = winreg.QueryValueEx(clsid_key, None)[0]
        with winreg.OpenKey(
            winreg.HKEY_CLASSES_ROOT, rf"CLSID\{clsid}\LocalServer32"
        ) as server_key:
            command = winreg.QueryValueEx(server_key, None)[0]
        return _windows_file_version(_server_executable(str(command)))
    except (OSError, ValueError):
        return None


def _probe() -> int:
    health = "ready"
    detail = "Origin COM 与托管 Python 运行时可用"
    software_version = None
    try:
        if sys.platform != "win32":
            raise RuntimeError("Origin adapter requires Windows")
        import winreg

        import originpro

        with winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, r"Origin.ApplicationSI\CLSID"):
            pass
        software_version = _registered_origin_version(winreg)
        originpro_version = version("originpro")
        detail = f"Origin COM 与托管 Python 运行时可用（originpro {originpro_version}）"
        del originpro
    except (
        FileNotFoundError,
        ImportError,
        OSError,
        PackageNotFoundError,
        RuntimeError,
    ) as exc:
        health = "unavailable"
        detail = str(exc)
    print(
        json.dumps(
            {
                "adapter_version": ADAPTER_VERSION,
                "software": "OriginPro",
                "software_version": software_version,
                "health": health,
                "detail": detail,
            },
            ensure_ascii=False,
        )
    )
    return 0


def _validate_semantics(request: dict[str, Any]) -> None:
    input_keys = {item["key"] for item in request["inputs"]}
    plot = request["operation"]["plot"]
    plot_type = plot["type"]
    if plot_type in COMPOSITION_PLOT_TYPES:
        panels = plot["panels"]
        layout = plot["layout"]
        grid = (layout["rows"], layout["columns"])
        valid_grids = {
            1: {(1, 1)},
            2: {(1, 2), (2, 1)},
            3: {(2, 2)},
            4: {(2, 2)},
        }
        if grid not in valid_grids[len(panels)]:
            raise ValueError("PANEL_LAYOUT_TOO_SMALL")
        panel_keys = [panel["key"] for panel in panels]
        if len(set(panel_keys)) != len(panel_keys):
            raise ValueError("PANEL_KEYS_MUST_BE_UNIQUE")
        series = [item for panel in panels for item in panel["series"]]
        if len(series) > 16:
            raise ValueError("SERIES_COUNT_EXCEEDED")
        for panel in panels:
            if not any(item.get("y_axis", "left") == "left" for item in panel["series"]):
                raise ValueError("PANEL_LEFT_AXIS_REQUIRES_SERIES")
            for axis_name in ("x_axis", "y_axis", "right_y_axis"):
                _validate_axis(panel.get(axis_name), axis_name)
            _validate_annotations(panel.get("annotations"))
        for axis_name in ("x_axis", "y_axis"):
            _validate_axis(plot.get(axis_name), axis_name)
        for axis_name, share_name in (("x_axis", "share_x"), ("y_axis", "share_y")):
            if layout.get(share_name):
                settings = [_panel_setting(plot, panel, axis_name) or {} for panel in panels]
                if any(value != settings[0] for value in settings[1:]):
                    raise ValueError(f"SHARED_{axis_name.upper()}_CONFIG_MISMATCH")
    else:
        series = plot["series"]
        _validate_annotations(plot.get("annotations"))
    used_inputs = {item["input"] for item in series}
    if used_inputs != input_keys:
        raise ValueError("INPUT_BINDINGS_MUST_BE_USED_EXACTLY")
    if plot_type in COMPOSITION_PLOT_TYPES:
        return
    if plot_type == "grouped_column" and len(series) < 2:
        raise ValueError("GROUPED_COLUMN_REQUIRES_MULTIPLE_SERIES")
    if plot_type in STACKED_PLOT_TYPES and len(series) < 2:
        raise ValueError("STACKED_PLOT_REQUIRES_MULTIPLE_SERIES")
    if plot_type in OFFSET_STACKED_PLOT_TYPES and len(series) < 2:
        raise ValueError("STACKED_LINE_REQUIRES_MULTIPLE_SERIES")
    if plot_type in OFFSET_STACKED_PLOT_TYPES and (
        (plot.get("y_axis") or {}).get("scale") in {"log10", "ln", "log2"}
    ):
        raise ValueError("STACKED_LINE_LOG_Y_UNSUPPORTED")
    if plot_type == "violin" and any("style" in item for item in series):
        raise ValueError("VIOLIN_SERIES_STYLE_UNSUPPORTED")
    if plot_type in XYZ_PLOT_TYPES and len(series) != 1:
        raise ValueError("XYZ_PLOT_REQUIRES_ONE_SERIES")
    required_roles = (
        ("x", "y", "z")
        if plot_type in XYZ_PLOT_TYPES
        else ("x", "y", "y_error")
        if plot_type == "y_error"
        else ("y",)
        if plot_type in {"box", "histogram", "violin"}
        else ("x", "y", "size")
        if plot_type == "bubble"
        else ("x", "y", "lower", "upper")
        if plot_type == "band"
        else ("x", "y")
    )
    for item in series:
        if any(role not in item for role in required_roles):
            raise ValueError("SERIES_REQUIRED_ROLE_MISSING")
    for axis_name in ("x_axis", "y_axis", "z_axis"):
        _validate_axis(plot.get(axis_name), axis_name)


def _validate_annotations(value: Any) -> None:
    for item in value if isinstance(value, list) else []:
        numeric_fields = (
            ("value",) if item["kind"] == "reference_line"
            else ("from", "to") if item["kind"] == "reference_band"
            else ("x", "y")
        )
        if any(not math.isfinite(float(item[name])) for name in numeric_fields):
            raise ValueError("ANNOTATION_COORDINATE_NOT_FINITE")
        if item["kind"] == "reference_band" and item["from"] >= item["to"]:
            raise ValueError("ANNOTATION_BAND_RANGE_INVALID")


def _validate_axis(value: Any, name: str) -> None:
    axis = value if isinstance(value, dict) else {}
    minimum = axis.get("minimum")
    maximum = axis.get("maximum")
    if minimum is not None and maximum is not None and minimum >= maximum:
        raise ValueError(f"{name.upper()}_LIMITS_INVALID")
    if axis.get("scale") in {"log10", "ln", "log2"} and (
        minimum is not None and minimum <= 0 or maximum is not None and maximum <= 0
    ):
        raise ValueError(f"{name.upper()}_LOG_LIMIT_INVALID")


def _atomic_json(path: Path, value: Any) -> None:
    temporary = path.with_name(path.name + ".tmp-" + os.urandom(8).hex())
    try:
        with temporary.open("w", encoding="utf-8", newline="\n") as handle:
            json.dump(value, handle, ensure_ascii=False, indent=2)
            handle.flush()
            os.fsync(handle.fileno())
        os.replace(temporary, path)
    finally:
        temporary.unlink(missing_ok=True)


def _read_rows(path: Path, sheet: str | None) -> tuple[list[str], list[list[Any]]]:
    suffix = path.suffix.lower()
    if suffix == ".csv":
        with path.open("r", encoding="utf-8-sig", newline="") as handle:
            rows = list(csv.reader(handle))
        if len(rows) < 2:
            raise ValueError("CSV_INPUT_EMPTY")
        return [str(item) for item in rows[0]], rows[1:]
    if suffix == ".json":
        value = json.loads(path.read_text(encoding="utf-8"))
        if isinstance(value, list) and value and all(isinstance(item, dict) for item in value):
            headers = list(value[0])
            return headers, [[item.get(name) for name in headers] for item in value]
        if (
            isinstance(value, dict)
            and value
            and all(isinstance(item, list) for item in value.values())
        ):
            headers = list(value)
            length = max(len(value[name]) for name in headers)
            return headers, [
                [value[name][index] if index < len(value[name]) else None for name in headers]
                for index in range(length)
            ]
        raise ValueError("JSON_INPUT_SHAPE_UNSUPPORTED")
    if suffix == ".xlsx":
        from openpyxl import load_workbook

        workbook = load_workbook(path, read_only=True, data_only=True)
        try:
            worksheet = workbook[sheet] if sheet else workbook.active
            rows = list(worksheet.iter_rows(values_only=True))
        finally:
            workbook.close()
        if len(rows) < 2:
            raise ValueError("XLSX_INPUT_EMPTY")
        return [str(item or "") for item in rows[0]], [list(row) for row in rows[1:]]
    raise ValueError("INPUT_TYPE_UNSUPPORTED")


def _column_index(headers: list[str], value: Any, field: str) -> int:
    if not isinstance(value, str) or value not in headers:
        raise ValueError(f"{field.upper()}_COLUMN_NOT_FOUND")
    return headers.index(value)


def _manifest(path: Path, media_type: str) -> dict[str, Any]:
    return {
        "artifact_id": {
            "project.opju": "project",
            "figure.png": "figure_png",
            "figure.svg": "figure_svg",
            "figure.pdf": "figure_pdf",
            "plot-spec.json": "plot_spec",
            "provenance.json": "provenance",
        }[path.name],
        "filename": path.name,
        "media_type": media_type,
        "size_bytes": path.stat().st_size,
        "sha256": _file_sha256(path),
    }


def _file_sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def _axis_title(axis: Any, fallback: str) -> str:
    if not isinstance(axis, dict):
        return fallback
    title = str(axis.get("title") or fallback)
    unit = str(axis.get("unit") or "")
    return f"{title} ({unit})" if unit else title


def _hex_color(value: str) -> tuple[int, int, int]:
    return tuple(int(value[index : index + 2], 16) for index in (1, 3, 5))


def _apply_canvas(graph: Any, canvas: Any) -> None:
    if not isinstance(canvas, dict):
        return
    width = float(canvas["width_mm"])
    height = float(canvas["height_mm"])
    # Origin may reload a sub-600-DPI template against the active printer and
    # silently change resx/resy without scaling width/height. Pin both the
    # physical page and its resolution before any export or OPJU save.
    graph.activate()
    graph.obj.LT_execute(
        "page.updatetoprinter=0; "
        "page.resx=600; page.resy=600; "
        f"page.width={width / 25.4 * 600}; "
        f"page.height={height / 25.4 * 600};"
    )


def _new_graph(op: Any, template: str, canvas: Any) -> Any:
    """Create against the final page size so template objects stay inside exports."""
    graph = op.new_graph(template=template)
    if not isinstance(canvas, dict):
        return graph
    # Origin templates, especially 3D surfaces, may store layer geometry in
    # physical units. Convert it to % of page before changing page dimensions;
    # otherwise a smaller requested canvas crops axes and the color scale.
    for layer in graph:
        layer.set_int("unit", 1)
    _apply_canvas(graph, canvas)
    return graph


def _configure_origin_session(op: Any) -> None:
    """Normalize export behavior that otherwise depends on machine defaults."""
    # Keep labels flush with their frames. Origin 2024 still emits a separate
    # baseline drawing primitive; exported files are normalized below.
    op.set_lt_var("@U", 1)


_SVG_TEXT_BASELINE_RE = re.compile(
    rb'(?P<text><text\b(?P<attrs>[^>]*)>.*?</text>\s*)'
    rb'(?P<path><path\s+d="M\s*(?P<x1>-?[\d.]+),(?P<y1>-?[\d.]+)\s+'
    rb'L\s*(?P<x2>-?[\d.]+),(?P<y2>-?[\d.]+)"\s+'
    rb'style="stroke:\s*black;stroke-width:\s*(?P<width>[\d.]+);[^\"]*"'
    rb'\s+fill="none"[^>]*/>)',
    re.DOTALL,
)


def _strip_origin_svg_text_baselines(
    content: bytes,
) -> tuple[bytes, list[tuple[float, float, float, float, float]]]:
    """Remove Origin 2024's separate printing-baseline paths after text nodes."""
    baselines: list[tuple[float, float, float, float, float]] = []

    def replace(match: re.Match[bytes]) -> bytes:
        attrs = match.group("attrs")
        x1, y1, x2, y2, width = (
            float(match.group(name)) for name in ("x1", "y1", "x2", "y2", "width")
        )
        font_match = re.search(rb'font-size="([\d.]+)"', attrs)
        if font_match is None:
            return match.group(0)
        font_size = float(font_match.group(1))
        transform = re.search(
            rb'transform="matrix\([^\"]+\s(-?[\d.]+)\s(-?[\d.]+)\)"', attrs
        )
        text_x = re.search(rb'\bx="(-?[\d.]+)"', attrs)
        text_y = re.search(rb'\by="(-?[\d.]+)"', attrs)
        horizontal = abs(y1 - y2) < 0.01
        vertical = abs(x1 - x2) < 0.01
        matches_text_frame = False
        if horizontal and text_x and text_y:
            expected_x = float(text_x.group(1))
            expected_y = float(text_y.group(1)) - 0.78 * font_size
            matches_text_frame = (
                abs(x1 - expected_x) <= max(2, font_size * 0.08)
                and abs(y1 - expected_y) <= max(3, font_size * 0.12)
            )
        elif vertical and transform:
            translate_x, translate_y = map(float, transform.groups())
            expected_x = translate_x - 0.78 * font_size
            matches_text_frame = (
                abs(x1 - expected_x) <= max(3, font_size * 0.12)
                and abs(y1 - translate_y) <= max(3, font_size * 0.08)
            )
        if not matches_text_frame or max(abs(x2 - x1), abs(y2 - y1)) < 1.5 * font_size:
            return match.group(0)
        if not 0.04 <= width / font_size <= 0.14:
            return match.group(0)
        baselines.append((x1, y1, x2, y2, width))
        return match.group("text")

    return _SVG_TEXT_BASELINE_RE.sub(replace, content), baselines


_PDF_BASELINE_RE = re.compile(
    rb'(?m)(?P<width>[\d.]+) w\s*\n0 J\s*\n'
    rb'(?P<x1>-?[\d.]+) (?P<y1>-?[\d.]+) m\s*\n'
    rb'(?P<x2>-?[\d.]+) (?P<y2>-?[\d.]+) l\s*\nS\s*\nQ'
)


def _strip_origin_pdf_text_baselines(path: Path) -> int:
    """Remove isolated long black text-baseline strokes without rewriting xrefs."""
    content = bytearray(path.read_bytes())
    removed = 0
    for stream_match in reversed(list(re.finditer(rb'stream\r?\n', content))):
        start = stream_match.end()
        end = content.find(b"endstream", start)
        if end < 0:
            continue
        compressed = bytes(content[start:end]).rstrip(b"\r\n")
        try:
            decoded = zlib.decompress(compressed)
        except zlib.error:
            continue

        def replace(match: re.Match[bytes]) -> bytes:
            nonlocal removed
            width, x1, y1, x2, y2 = (
                float(match.group(name))
                for name in ("width", "x1", "y1", "x2", "y2")
            )
            length = max(abs(x2 - x1), abs(y2 - y1))
            if min(abs(x2 - x1), abs(y2 - y1)) > 0.02 or length < 10 * width:
                return match.group(0)
            removed += 1
            replacement = b"Q"
            return replacement + b" " * (len(match.group(0)) - len(replacement))

        normalized = _PDF_BASELINE_RE.sub(replace, decoded)
        if normalized == decoded:
            continue
        recompressed = zlib.compress(normalized, 9)
        if len(recompressed) > len(compressed):
            raise RuntimeError("PDF_BASELINE_NORMALIZATION_FAILED")
        content[start : start + len(compressed)] = recompressed + b"\n" * (
            len(compressed) - len(recompressed)
        )
    if removed:
        path.write_bytes(content)
    return removed


def _render_pdf_to_png(
    pdf_path: Path, png_path: Path, pixel_width: int
) -> tuple[int, int]:
    """Rasterize the normalized vector export instead of Origin's broken PNG."""
    import pymupdf

    with pymupdf.open(pdf_path) as document:
        if document.page_count != 1:
            raise RuntimeError("PNG_SOURCE_PDF_PAGE_COUNT_INVALID")
        page = document[0]
        scale = pixel_width / page.rect.width
        pixmap = page.get_pixmap(matrix=pymupdf.Matrix(scale, scale), alpha=False)
        pixmap.save(png_path)
        return pixmap.width, pixmap.height


def _arrange_panel_layers(
    layers: list[Any], grid: tuple[int, int]
) -> list[tuple[float, float, float, float]]:
    """Let Origin arrange the grid, then return its resulting page geometry."""
    settings = PANEL_LAYOUT[grid]
    for layer in layers:
        layer.set_int("unit", 1)
    layers[0].activate()
    arguments = " ".join(
        f"{name}:={value}"
        for name, value in (
            ("row", grid[0]),
            ("col", grid[1]),
            *settings.items(),
        )
    )
    layers[0].obj.LT_execute(f"layarrange {arguments};")
    return [
        tuple(layer.get_float(name) for name in ("left", "top", "width", "height"))
        for layer in layers
    ]


def _apply_shared_x_presentation(
    layer: Any, *, panel_index: int, rows: int, columns: int, share_x: bool
) -> None:
    """For shared X grids, keep labels and the title only on the bottom row."""
    if not share_x or panel_index // columns == rows - 1:
        return
    title = layer.label("xb")
    if title is not None:
        title.show = False
    layer.activate()
    layer.obj.LT_execute("axis -ps X L 0;")


def _apply_axis(layer: Any, name: str, spec: Any, fallback: str) -> None:
    settings = spec if isinstance(spec, dict) else {}
    axis = layer.axis(name)
    axis.title = _axis_title(settings, fallback)
    if "scale" in settings:
        axis.scale = settings["scale"]
    if any(key in settings for key in ("minimum", "maximum", "major_step")):
        axis.set_limits(
            settings.get("minimum"), settings.get("maximum"), settings.get("major_step")
        )
    if "tick_label_angle" in settings:
        layer.set_float(f"{name}.label.rotate", float(settings["tick_label_angle"]))
    if "tick_label_font_size" in settings:
        layer.set_float(f"{name}.label.pt", float(settings["tick_label_font_size"]))
    if "title_font_size" in settings:
        title_object = {"x": "xb", "y": "yl", "y2": "yr", "z": "zf"}[name]
        layer.label(title_object).set_int("fsize", round(settings["title_font_size"]))
    if "minor_ticks" in settings:
        layer.set_int(f"{name}.minorTicks", int(settings["minor_ticks"]))
    if "reverse" in settings:
        layer.set_int(f"{name}.reverse", int(settings["reverse"]))
    if "grid" in settings:
        grid_value = {"none": 0, "major": 1, "major_minor": 3}[settings["grid"]]
        layer.set_int(f"{name}.grid.show", grid_value)


def _lt_number(value: Any) -> str:
    number = float(value)
    if not math.isfinite(number):
        raise ValueError("ANNOTATION_COORDINATE_NOT_FINITE")
    return format(number, ".15g")


def _style_graphic_object(layer: Any, name: str, color: str) -> None:
    layer.obj.LT_execute(f'{name}.color=color("{color}");')


def _add_annotation_label(
    layer: Any,
    *,
    name: str,
    text: str,
    x: float,
    y: float,
    color: str,
    font_size: float,
    rotation: float = 0,
    background: str = "none",
) -> None:
    label = layer.add_label(text, x, y)
    label.name = name
    label.set_int("attach", 2)
    label.set_int("background", 4 if background == "white" else 0)
    label.set_int("fsize", round(font_size))
    label.set_float("rotate", float(rotation))
    _style_graphic_object(layer, name, color)


def _apply_annotations(layer: Any, annotations: Any) -> None:
    if not isinstance(annotations, list) or not annotations:
        return
    layer.activate()
    x_begin, x_end = layer.xlim
    y_begin, y_end = layer.ylim
    x_low, x_high = sorted((float(x_begin), float(x_end)))
    y_low, y_high = sorted((float(y_begin), float(y_end)))
    x_margin = (x_high - x_low) * 0.02
    y_margin = (y_high - y_low) * 0.02
    for index, item in enumerate(annotations, start=1):
        name = f"ZCBOT_ANN_{index:03d}"
        kind = item["kind"]
        color = item.get("color", "#666666")
        if kind == "text":
            _add_annotation_label(
                layer,
                name=name,
                text=item["text"],
                x=float(item["x"]),
                y=float(item["y"]),
                color=color,
                font_size=float(item.get("font_size", 10)),
                rotation=float(item.get("rotation", 0)),
                background=item.get("background", "none"),
            )
            continue
        if kind == "reference_line":
            orientation = "-v" if item["axis"] == "x" else "-h"
            layer.obj.LT_execute(
                f"draw -n {name} -l {orientation} {_lt_number(item['value'])};"
                f'{name}.color=color("{color}");'
                f"{name}.lineWidth={_lt_number(item.get('line_width', 1))};"
                f"{name}.lineType={LINE_STYLES[item.get('line_style', 'dash')]};"
                f"{name}.attach=2;{name}.clip=1;"
            )
            if item.get("label"):
                x = float(item["value"]) + x_margin if item["axis"] == "x" else x_low + x_margin
                y = y_high - y_margin if item["axis"] == "x" else float(item["value"]) + y_margin
                _add_annotation_label(
                    layer,
                    name=f"{name}_LABEL",
                    text=item["label"],
                    x=x,
                    y=y,
                    color=color,
                    font_size=float(item.get("label_font_size", 9)),
                )
            continue
        low = float(item["from"])
        high = float(item["to"])
        corners = (
            ((low, y_low), (high, y_low), (high, y_high), (low, y_high))
            if item["axis"] == "x"
            else ((x_low, low), (x_high, low), (x_high, high), (x_low, high))
        )
        coordinates = "".join(
            f"{name}.x{point}={_lt_number(x)};{name}.y{point}={_lt_number(y)};"
            for point, (x, y) in enumerate(corners, start=1)
        )
        layer.obj.LT_execute(
            f"draw -n {name} -b;{name}.attach=2;{coordinates}"
            f'{name}.color=color("{color}");{name}.fillColor=color("{color}");'
            f"{name}.transparency={int(item.get('transparency', 80))};"
            f"{name}.clip=1;"
        )
        if item.get("label"):
            x = (low + high) / 2 if item["axis"] == "x" else x_low + x_margin
            y = y_high - y_margin if item["axis"] == "x" else (low + high) / 2
            _add_annotation_label(
                layer,
                name=f"{name}_LABEL",
                text=item["label"],
                x=x,
                y=y,
                color=color,
                font_size=float(item.get("label_font_size", 9)),
            )


def _rescale_with_axis_scales(layer: Any, axes: list[tuple[str, Any]]) -> None:
    """Apply axis transforms before Origin derives automatic limits.

    Origin's linear rescale commonly includes zero. Switching that result to a
    logarithmic axis afterwards coerces the zero bound to an unusable value such
    as 1E-10. Explicit limits are intentionally left to ``_apply_axis`` after
    rescaling so a caller-provided range remains authoritative.
    """
    for name, spec in axes:
        if isinstance(spec, dict) and "scale" in spec:
            layer.axis(name).scale = spec["scale"]
    layer.rescale()


def _apply_series_style(origin_plot: Any, style: Any) -> None:
    if not isinstance(style, dict):
        return
    if "color" in style:
        origin_plot.color = _hex_color(style["color"])
    if "line_width" in style:
        origin_plot.set_float("line.width", float(style["line_width"]))
    if "line_style" in style:
        origin_plot.set_int("line.type", LINE_STYLES[style["line_style"]])
    if "symbol" in style:
        origin_plot.symbol_kind = SYMBOLS[style["symbol"]]
    if "symbol_size" in style:
        origin_plot.symbol_size = float(style["symbol_size"])
    if "transparency" in style:
        origin_plot.transparency = int(style["transparency"])


def _apply_error_style(origin_plot: Any, style: Any) -> None:
    if not isinstance(style, dict):
        return
    if "color" in style:
        origin_plot.color = _hex_color(style["color"])
    if "line_width" in style:
        origin_plot.set_float("line.width", float(style["line_width"]))
    if "transparency" in style:
        origin_plot.transparency = int(style["transparency"])


def _apply_legend(
    layer: Any,
    legend: Any,
    vertical_offset: int = 0,
    *,
    attach_to_layer: bool = False,
    graph: Any = None,
    geometry: tuple[int, int, int, int] | None = None,
) -> None:
    if legend is None:
        legend = {"enabled": True, "position": "auto"}
    elif not isinstance(legend, dict):
        return
    label = _legend_label(layer)
    label.set_int("background", 0)
    if attach_to_layer:
        label.set_int("attach", 0)
    label.show = legend.get("enabled", True)
    if "font_size" in legend:
        label.set_int("fsize", round(legend["font_size"]))
    if legend.get("position") == "auto":
        label.set_int("smartpos", 1)
    elif "position" in legend:
        if attach_to_layer:
            # Fixed page coordinates disable Origin's collision avoidance.
            # Delegate multi-panel placement even for historical requests
            # that specified a corner before automatic layout existed.
            label.set_int("attach", 0)
            label.set_int("smartpos", 1)
        else:
            left, top = LEGEND_POSITIONS[legend["position"]]
            if vertical_offset and legend["position"].startswith("bottom"):
                vertical_offset = -vertical_offset
            label.set_int("left", left)
            label.set_int("top", top + vertical_offset)


def _replace_series_legend(
    layer: Any,
    series_specs: list[dict[str, Any]],
    origin_plots: list[Any] | None = None,
) -> None:
    plot_numbers = (
        [plot.index() + 1 for plot in origin_plots]
        if origin_plots is not None
        else list(range(1, len(series_specs) + 1))
    )
    _legend_label(layer).text = "\n".join(
        f"\\l({plot_number}) {item.get('label') or item['y']}"
        for plot_number, item in zip(plot_numbers, series_specs, strict=True)
    )


def _legend_label(layer: Any) -> Any:
    label = layer.label("Legend")
    if label is None:
        label = layer.add_label("")
        label.name = "Legend"
    return label


def _apply_surface_3d_presentation(
    layer: Any,
    legend: Any,
    *,
    x_title: str | None = None,
    y_title: str | None = None,
    z_title: str | None = None,
) -> None:
    """Keep the projected frame and its optional color scale inside the page."""
    layer.set_int("unit", 1)
    for name, value in (
        ("left", 22),
        ("top", 15),
        ("width", 58),
        ("height", 62),
    ):
        layer.set_float(name, value)
    for axis_name in ("x", "y", "z"):
        layer.set_float(f"{axis_name}.label.pt", 7)
    # The glCMAP template keeps front/back title objects for every dimension,
    # while originpro's generic title setter only updates one face. Delete all
    # six template titles, then use ordinary layer-attached labels that Origin
    # will not regenerate or move to an unused 3D face.
    layer.obj.LT_execute(
        "label -r xb; label -r xt; label -r yl; "
        "label -r yr; label -r zb; label -r zf;"
    )
    for name, text, left, top, rotate in (
        ("ZCBOT_X_TITLE", x_title, 1000, 10100, 0),
        ("ZCBOT_Y_TITLE", y_title, 7600, 9700, 0),
        ("ZCBOT_Z_TITLE", z_title, -1300, 3500, 90),
    ):
        if text is None:
            continue
        title = layer.add_label(text)
        title.name = name
        title.set_int("attach", 0)
        title.set_int("background", 0)
        title.set_int("fsize", 9)
        title.set_int("left", left)
        title.set_int("top", top)
        title.set_int("rotate", rotate)

    # A 3D colormap surface uses Spectrum1 as its legend, not the ordinary
    # Legend label. Respect the same public legend.enabled contract.
    color_scale = layer.label("Spectrum1")
    if color_scale is not None and isinstance(legend, dict):
        color_scale.show = legend.get("enabled", True)


def _apply_title(
    layer: Any, value: Any, style: Any, *, graph: Any = None, top: int = 120
) -> None:
    if not value:
        return
    title = layer.add_label(str(value))
    title.set_int("background", 0)
    title.set_int("fsize", round((style or {}).get("font_size", 18)))
    title.set_int("left", round(graph.get_float("width") * 0.25) if graph else 2200)
    title.set_int("top", top)


def _panel_page_pixel(
    graph: Any,
    geometry: tuple[int, int, int, int] | None,
    x_fraction: float,
    y_fraction: float,
) -> tuple[float, float]:
    if geometry is None:
        raise ValueError("PANEL_GEOMETRY_REQUIRED")
    page_width = graph.get_float("width")
    page_height = graph.get_float("height")
    left, top, width, height = geometry
    return (
        page_width * (left + width * x_fraction) / 100,
        page_height * (top + height * y_fraction) / 100,
    )


def _apply_panel_title(
    graph: Any,
    layer: Any,
    geometry: tuple[int, int, int, int],
    value: Any,
    style: Any,
) -> None:
    if not value:
        return
    title = layer.add_label(str(value))
    title.set_int("background", 0)
    title.set_int("attach", 1)
    title.set_int("fsize", round((style or {}).get("font_size", 12)))
    left, top = _panel_page_pixel(graph, geometry, 0.28, -0.14)
    title.set_int("left", round(left))
    title.set_int("top", round(top))


def _apply_page_title(graph: Any, layer: Any, value: Any, style: Any) -> None:
    if not value:
        return
    title = layer.add_label(str(value))
    title.set_int("background", 0)
    title.set_int("attach", 1)
    title.set_int("fsize", round((style or {}).get("font_size", 18)))
    title.set_int("left", round(graph.get_float("width") * 0.32))
    title.set_int("top", round(graph.get_float("height") * 0.01))


def _panel_setting(plot_spec: dict[str, Any], panel: dict[str, Any], name: str) -> Any:
    value = panel.get(name)
    return value if value is not None else plot_spec.get(name)


def _panel_y_axis_fallback(panel: dict[str, Any], series_specs: list[dict[str, Any]]) -> str:
    """Prefer meaningful declarative metadata over Origin's generic ``Y``."""
    return str(panel.get("title") or series_specs[0].get("y") or "Y")


def _panel_legend(plot_spec: dict[str, Any], panel: dict[str, Any]) -> dict[str, Any]:
    return {
        "enabled": True,
        "position": "top_right",
        **(plot_spec.get("legend") or {}),
        **(panel.get("legend") or {}),
    }


def _input_file(job_dir: Path, key: str) -> Path:
    directory = job_dir / "input" / key
    files = [
        path for path in directory.iterdir() if path.is_file() and not path.name.startswith(".")
    ]
    if len(files) != 1:
        raise ValueError(f"INPUT_FILE_COUNT_INVALID:{key}")
    return files[0]


def _resolve_series(
    input_data: dict[str, tuple[list[str], list[list[Any]]]],
    series_specs: list[dict[str, Any]],
) -> tuple[list[dict[str, Any]], dict[tuple[str, int], str]]:
    resolved: list[dict[str, Any]] = []
    labels: dict[tuple[str, int], str] = {}
    for series in series_specs:
        input_key = series["input"]
        headers, _ = input_data[input_key]
        role_indexes = {
            role: _column_index(headers, series[role], role)
            for role in ("x", "y", "z", "size", "lower", "upper", "x_error", "y_error")
            if role in series
        }
        y_index = role_indexes["y"]
        label = series.get("label")
        label_key = (input_key, y_index)
        effective_label = label or series["y"]
        if label_key in labels and labels[label_key] != effective_label:
            raise ValueError("SERIES_LABEL_CONFLICT")
        labels[label_key] = effective_label
        resolved.append({"input": input_key, "label": label, **role_indexes})
    return resolved, labels


def _number(value: Any, role: str) -> float:
    if isinstance(value, bool):
        raise ValueError(f"{role.upper()}_VALUE_NOT_NUMERIC")  # noqa: TRY004
    try:
        number = float(value)
    except (TypeError, ValueError) as exc:
        raise ValueError(f"{role.upper()}_VALUE_NOT_NUMERIC") from exc
    if not math.isfinite(number):
        raise ValueError(f"{role.upper()}_VALUE_NOT_FINITE")
    return number


def _validate_bubble_values(
    input_data: dict[str, tuple[list[str], list[list[Any]]]],
    resolved_series: list[dict[str, Any]],
) -> None:
    for resolved in resolved_series:
        _, rows = input_data[resolved["input"]]
        for row in rows:
            try:
                size = _number(row[resolved["size"]], "size")
            except IndexError as exc:
                raise ValueError("BUBBLE_ROW_INCOMPLETE") from exc
            if size <= 0:
                raise ValueError("SIZE_VALUE_NOT_POSITIVE")


def _validate_band_values(
    input_data: dict[str, tuple[list[str], list[list[Any]]]],
    resolved_series: list[dict[str, Any]],
) -> None:
    for resolved in resolved_series:
        _, rows = input_data[resolved["input"]]
        for row in rows:
            try:
                lower = _number(row[resolved["lower"]], "lower")
                upper = _number(row[resolved["upper"]], "upper")
            except IndexError as exc:
                raise ValueError("BAND_ROW_INCOMPLETE") from exc
            if lower > upper:
                raise ValueError("BAND_BOUNDS_INVERTED")


def _is_evenly_spaced(values: list[float]) -> bool:
    if len(values) <= 2:
        return True
    step = values[1] - values[0]
    tolerance = max(abs(step) * 1e-9, 1e-12)
    return all(
        math.isclose(current - previous, step, rel_tol=1e-9, abs_tol=tolerance)
        for previous, current in pairwise(values[1:])
    )


def _heatmap_matrix(
    rows: list[list[Any]], resolved: dict[str, Any]
) -> tuple[list[list[float]], tuple[float, float, float, float]]:
    points: dict[tuple[float, float], float] = {}
    for row in rows:
        try:
            x = _number(row[resolved["x"]], "x")
            y = _number(row[resolved["y"]], "y")
            z = _number(row[resolved["z"]], "z")
        except IndexError as exc:
            raise ValueError("HEATMAP_ROW_INCOMPLETE") from exc
        if (x, y) in points:
            raise ValueError("HEATMAP_COORDINATES_DUPLICATED")
        points[(x, y)] = z
    x_values = sorted({item[0] for item in points})
    y_values = sorted({item[1] for item in points})
    if len(x_values) < 2 or len(y_values) < 2:
        raise ValueError("HEATMAP_GRID_TOO_SMALL")
    if len(points) != len(x_values) * len(y_values):
        raise ValueError("HEATMAP_GRID_INCOMPLETE")
    if not _is_evenly_spaced(x_values) or not _is_evenly_spaced(y_values):
        raise ValueError("HEATMAP_GRID_NOT_REGULAR")
    matrix = [[points[(x, y)] for x in x_values] for y in y_values]
    return matrix, (x_values[0], x_values[-1], y_values[0], y_values[-1])


def _validate_artifact(path: Path, extension: str) -> None:
    if not path.is_file() or path.stat().st_size == 0:
        raise RuntimeError(f"{extension.upper()}_EXPORT_EMPTY")
    head = path.read_bytes()[:1024]
    if extension == "png" and not head.startswith(b"\x89PNG\r\n\x1a\n"):
        raise RuntimeError("PNG_EXPORT_INVALID")
    if extension == "pdf" and not head.startswith(b"%PDF-"):
        raise RuntimeError("PDF_EXPORT_INVALID")
    if extension == "svg" and b"<svg" not in head.lower():
        raise RuntimeError("SVG_EXPORT_INVALID")
    if extension == "opju" and len(head) < 64:
        raise RuntimeError("OPJU_EXPORT_INVALID")


def _add_xy_plots(
    layer: Any,
    worksheets: dict[str, Any],
    series_specs: list[dict[str, Any]],
    resolved_series: list[dict[str, Any]],
) -> list[Any]:
    plots = []
    error_plots: list[list[Any]] = []
    for series_spec, resolved in zip(series_specs, resolved_series, strict=True):
        kind = series_spec["kind"]
        arguments = {
            "coly": resolved["y"],
            "colx": resolved["x"],
            "type": PLOT_CONFIG[kind][1],
        }
        if "x_error" in resolved:
            arguments["colxerr"] = resolved["x_error"]
        if "y_error" in resolved:
            arguments["colyerr"] = resolved["y_error"]
        plot_count = len(layer.plot_list())
        plot = layer.add_plot(worksheets[resolved["input"]], **arguments)
        plots.append(plot)
        error_plots.append(list(layer.plot_list())[plot_count + 1 :])
    if len(plots) > 1 and all(item["kind"] == "column" for item in series_specs):
        layer.group()
    else:
        # Explicit series styles require independent plots.
        layer.group(False)
    for plot, errors, series_spec in zip(plots, error_plots, series_specs, strict=True):
        style = series_spec.get("style")
        _apply_series_style(plot, style)
        for error_plot in errors:
            _apply_error_style(error_plot, style)
    return plots


def _configure_bubble_plot(
    op: Any,
    origin_plot: Any,
    series_spec: dict[str, Any],
    resolved: dict[str, Any],
) -> None:
    origin_plot.symbol_size = op.modi_col(resolved["size"] - resolved["y"])
    origin_plot.symbol_sizefactor = float((series_spec.get("style") or {}).get("symbol_size", 10))


def _build_stacked_graph(
    op: Any,
    input_data: dict[str, tuple[list[str], list[list[Any]]]],
    series_specs: list[dict[str, Any]],
    resolved_series: list[dict[str, Any]],
    template: str,
    plot_type: int,
    canvas: Any = None,
) -> tuple[Any, Any, list[Any]]:
    first = resolved_series[0]
    _, first_rows = input_data[first["input"]]
    x_values = [row[first["x"]] for row in first_rows]
    y_columns: list[list[Any]] = []
    for resolved in resolved_series:
        _, rows = input_data[resolved["input"]]
        current_x = [row[resolved["x"]] for row in rows]
        if current_x != x_values:
            raise ValueError("STACKED_PLOT_X_VALUES_MISMATCH")
        y_columns.append([row[resolved["y"]] for row in rows])

    staging = op.new_sheet("w", lname="stacked_plot_data")
    staging.from_list(0, x_values, lname=str(series_specs[0]["x"]))
    for index, (series_spec, values) in enumerate(
        zip(series_specs, y_columns, strict=True), start=1
    ):
        staging.from_list(
            index,
            values,
            lname=str(series_spec.get("label") or series_spec["y"]),
        )

    graph = _new_graph(op, template, canvas)
    layer = graph[0]
    data_range = f"{staging.lt_range(False)}!(1,2:{len(series_specs) + 1})"
    layer.add_plot(data_range, type=plot_type)
    layer.group(True, 0, len(series_specs) - 1)
    layer.activate()
    layer.obj.LT_execute(_CUMULATIVE_STACK_COMMAND)
    return graph, layer, list(layer.plot_list())


def _build_violin_graph(
    op: Any,
    input_data: dict[str, tuple[list[str], list[list[Any]]]],
    series_specs: list[dict[str, Any]],
    resolved_series: list[dict[str, Any]],
    canvas: Any = None,
) -> tuple[Any, Any, list[Any]]:
    """Submit all raw Y columns as one range so Origin builds a categorical axis."""
    staging = op.new_sheet("w", lname="violin_plot_data")
    for index, (series_spec, resolved) in enumerate(
        zip(series_specs, resolved_series, strict=True)
    ):
        _, rows = input_data[resolved["input"]]
        staging.from_list(
            index,
            [row[resolved["y"]] if resolved["y"] < len(row) else None for row in rows],
            lname=str(series_spec.get("label") or series_spec["y"]),
        )
    staging.cols_axis("y", 0, len(series_specs) - 1)

    staging.activate()
    staging.obj.LT_execute(
        f"worksheet -s 1 0 {len(series_specs)} 0; worksheet -p 206 Violin;"
    )
    graph = op.find_graph()
    if graph is None:
        raise RuntimeError("VIOLIN_GRAPH_MISSING")
    if isinstance(canvas, dict):
        for graph_layer in graph:
            graph_layer.set_int("unit", 1)
        _apply_canvas(graph, canvas)
    layer = graph[0]
    # Category labels are wider than numeric ticks; reserve enough room for
    # both the labels and the X-axis title on publication-sized canvases.
    layer.set_int("unit", 1)
    for name, value in (("left", 18), ("top", 14), ("width", 68), ("height", 58)):
        layer.set_float(name, value)
    origin_plots = list(layer.plot_list())
    if not origin_plots:
        raise RuntimeError("VIOLIN_PLOT_MISSING")
    return graph, layer, origin_plots


def _build_offset_stacked_line_graph(
    op: Any,
    input_data: dict[str, tuple[list[str], list[list[Any]]]],
    series_specs: list[dict[str, Any]],
    resolved_series: list[dict[str, Any]],
    gap_percent: float,
    canvas: Any = None,
) -> tuple[Any, Any, list[Any]]:
    values_by_series: list[tuple[list[float], list[float]]] = []
    maximum_span = 0.0
    for resolved in resolved_series:
        _, rows = input_data[resolved["input"]]
        x_values: list[float] = []
        y_values: list[float] = []
        for row in rows:
            try:
                x_values.append(_number(row[resolved["x"]], "x"))
                y_values.append(_number(row[resolved["y"]], "y"))
            except IndexError as exc:
                raise ValueError("STACKED_LINE_ROW_INCOMPLETE") from exc
        if not y_values:
            raise ValueError("STACKED_LINE_SERIES_EMPTY")
        maximum_span = max(maximum_span, max(y_values) - min(y_values))
        values_by_series.append((x_values, y_values))

    if maximum_span == 0:
        maximum_span = max(
            1.0,
            max(abs(value) for _, values in values_by_series for value in values),
        )
    offset_step = maximum_span * (1 + float(gap_percent) / 100)
    staging = op.new_sheet("w", lname="stacked_line_plot_data")
    graph = _new_graph(op, "line", canvas)
    layer = graph[0]
    # Spectral stacks need room below the bottom axis for large scientific
    # tick labels and a unit-bearing X title after PDF-to-PNG rendering.
    layer.set_int("unit", 1)
    for name, value in (
        ("left", 18),
        ("top", 14),
        ("width", 68),
        ("height", 60),
    ):
        layer.set_float(name, value)
    plots = []
    for index, (series_spec, (x_values, y_values)) in enumerate(
        zip(series_specs, values_by_series, strict=True)
    ):
        x_column = index * 2
        baseline = min(y_values)
        display_values = [value - baseline + index * offset_step for value in y_values]
        staging.from_list(x_column, x_values, lname=str(series_spec["x"]))
        staging.from_list(
            x_column + 1,
            display_values,
            lname=str(series_spec.get("label") or series_spec["y"]),
        )
        plots.append(layer.add_plot(staging, colx=x_column, coly=x_column + 1, type="l"))
        endpoint = max(range(len(x_values)), key=x_values.__getitem__)
        curve_label = layer.add_label(
            str(series_spec.get("label") or series_spec["y"]),
            x_values[endpoint],
            display_values[endpoint],
        )
        curve_label.name = f"ZCBOT_STACK_LABEL_{index + 1:03d}"
        curve_label.set_int("attach", 2)
        curve_label.set_int("background", 0)
        curve_label.set_int("fsize", 9)
    layer.group(False)
    return graph, layer, plots


def _share_axis_limits(layers: list[Any], name: str) -> None:
    limits = [getattr(layer, f"{name}lim") for layer in layers]
    begin = min(item[0] for item in limits)
    end = max(item[1] for item in limits)
    for layer in layers:
        getattr(layer, f"set_{name}lim")(begin, end)


def _build_recipe_graph(
    op: Any,
    plot_spec: dict[str, Any],
    worksheets: dict[str, Any],
    resolved_series: list[dict[str, Any]],
) -> Any:
    panels = plot_spec["panels"]
    layout = plot_spec["layout"]
    grid = (layout["rows"], layout["columns"])
    graph = _new_graph(op, "line", plot_spec.get("canvas"))
    while len(graph) < len(panels):
        graph.add_layer(0)
    primary_layers = list(graph)[: len(panels)]
    geometries = _arrange_panel_layers(primary_layers, grid)

    resolved_offset = 0
    for panel_index, (panel, layer, geometry) in enumerate(
        zip(panels, primary_layers, geometries, strict=True)
    ):
        panel_series = panel["series"]
        resolved_panel = resolved_series[resolved_offset : resolved_offset + len(panel_series)]
        resolved_offset += len(panel_series)
        left_pairs = [
            (spec, resolved)
            for spec, resolved in zip(panel_series, resolved_panel, strict=True)
            if spec.get("y_axis", "left") == "left"
        ]
        right_pairs = [
            (spec, resolved)
            for spec, resolved in zip(panel_series, resolved_panel, strict=True)
            if spec.get("y_axis", "left") == "right"
        ]
        left_specs = [item[0] for item in left_pairs]
        left_resolved = [item[1] for item in left_pairs]
        left_plots = _add_xy_plots(layer, worksheets, left_specs, left_resolved)
        x_axis = _panel_setting(plot_spec, panel, "x_axis")
        y_axis = _panel_setting(plot_spec, panel, "y_axis")
        _rescale_with_axis_scales(layer, [("x", x_axis), ("y", y_axis)])
        _apply_axis(
            layer,
            "x",
            x_axis,
            str(left_specs[0]["x"]),
        )
        _apply_axis(layer, "y", y_axis, _panel_y_axis_fallback(panel, left_specs))
        _apply_shared_x_presentation(
            layer,
            panel_index=panel_index,
            rows=grid[0],
            columns=grid[1],
            share_x=bool(layout.get("share_x")),
        )
        legend = _panel_legend(plot_spec, panel)
        _replace_series_legend(layer, left_specs, left_plots)
        _apply_legend(
            layer,
            legend,
            attach_to_layer=True,
            graph=graph,
            geometry=geometry,
        )
        _apply_panel_title(
            graph,
            layer,
            geometry,
            panel.get("title"),
            panel.get("title_style"),
        )
        if panel.get("panel_label"):
            panel_label = layer.add_label(str(panel["panel_label"]))
            panel_label.set_int("background", 0)
            panel_label.set_int("attach", 1)
            panel_label.set_int(
                "fsize", round((panel.get("title_style") or {}).get("font_size", 12))
            )
            panel_left, panel_top = _panel_page_pixel(graph, geometry, 0.02, -0.14)
            panel_label.set_float("left", round(panel_left))
            panel_label.set_float("top", round(panel_top))

        if right_pairs:
            layer.activate()
            right_layer = graph.add_layer(2)
            right_specs = [item[0] for item in right_pairs]
            right_resolved = [item[1] for item in right_pairs]
            right_plots = _add_xy_plots(right_layer, worksheets, right_specs, right_resolved)
            right_y_axis = panel.get("right_y_axis")
            _rescale_with_axis_scales(right_layer, [("y2", right_y_axis)])
            _apply_axis(right_layer, "y2", right_y_axis, "Right Y")
            _replace_series_legend(right_layer, right_specs, right_plots)
            _apply_legend(
                right_layer,
                legend,
                vertical_offset=700,
                attach_to_layer=True,
                graph=graph,
                geometry=geometry,
            )

    if layout.get("share_x") and len(primary_layers) > 1:
        _share_axis_limits(primary_layers, "x")
    if layout.get("share_y") and len(primary_layers) > 1:
        _share_axis_limits(primary_layers, "y")
    for panel, layer in zip(panels, primary_layers, strict=True):
        _apply_annotations(layer, panel.get("annotations"))
    _apply_page_title(
        graph,
        primary_layers[0],
        plot_spec.get("title"),
        plot_spec.get("title_style"),
    )
    return graph


def _build_band_graph(
    op: Any,
    worksheets: dict[str, Any],
    series_specs: list[dict[str, Any]],
    resolved_series: list[dict[str, Any]],
    canvas: Any = None,
) -> tuple[Any, Any, list[Any], str]:
    graph = _new_graph(op, "line", canvas)
    layer = graph[0]
    center_plots = []
    legend_entries = []
    for index, (series_spec, resolved) in enumerate(
        zip(series_specs, resolved_series, strict=True)
    ):
        worksheet = worksheets[resolved["input"]]
        upper_plot = layer.add_plot(worksheet, coly=resolved["upper"], colx=resolved["x"], type="l")
        lower_plot = layer.add_plot(worksheet, coly=resolved["lower"], colx=resolved["x"], type="l")
        upper_plot.set_fill_area(type=9)
        _apply_series_style(upper_plot, series_spec.get("style"))
        _apply_series_style(lower_plot, series_spec.get("style"))
        center_plot = layer.add_plot(worksheet, coly=resolved["y"], colx=resolved["x"], type="l")
        center_plots.append(center_plot)
        label = series_spec.get("label") or series_spec["y"]
        legend_entries.append(f"\\l({index * 3 + 3}) {label}")
    return graph, layer, center_plots, "\n".join(legend_entries)


def run(job_dir: Path) -> list[dict[str, Any]]:
    job_dir = job_dir.resolve(strict=True)
    request_record = json.loads((job_dir / "request" / "request.json").read_text(encoding="utf-8"))
    request = request_record["request"]
    _validate_semantics(request)
    input_specs = request["inputs"]
    input_files = {item["key"]: _input_file(job_dir, item["key"]) for item in input_specs}
    input_data = {
        item["key"]: _read_rows(
            input_files[item["key"]],
            (item.get("selector") or {}).get("sheet"),
        )
        for item in input_specs
    }
    plot_spec = request["operation"]["plot"]
    plot_type = plot_spec["type"]
    if plot_type not in PLOT_CONFIG and plot_type not in {
        "heatmap",
        *COMPOSITION_PLOT_TYPES,
    }:
        raise ValueError("PLOT_TYPE_NOT_IMPLEMENTED")
    series_specs = (
        [item for panel in plot_spec["panels"] for item in panel["series"]]
        if plot_type in COMPOSITION_PLOT_TYPES
        else plot_spec["series"]
    )
    resolved_series, labels = _resolve_series(input_data, series_specs)
    if plot_type == "bubble":
        _validate_bubble_values(input_data, resolved_series)
    if plot_type == "band":
        _validate_band_values(input_data, resolved_series)

    import originpro as op

    output = job_dir / "output"
    output.mkdir(exist_ok=True)
    op.set_show(False)
    try:
        workspace_path = job_dir / "workspace.json"
        workspace = (
            json.loads(workspace_path.read_text(encoding="utf-8"))
            if workspace_path.is_file()
            else {}
        )
        base_path = workspace.get("local_base_path")
        if workspace.get("mode") == "continue":
            if not isinstance(base_path, str):
                raise ValueError("WORKSPACE_BASE_MISSING")
            base_project = Path(base_path) / "project.opju"
            if not base_project.is_file():
                raise ValueError("WORKSPACE_PROJECT_MISSING")
            op.open(str(base_project))
        else:
            op.new()
        _configure_origin_session(op)
        worksheets: dict[str, Any] = {}
        for input_spec in input_specs:
            input_key = input_spec["key"]
            headers, rows = input_data[input_key]
            worksheet = op.new_sheet("w", lname=input_key)
            worksheets[input_key] = worksheet
            for index, header in enumerate(headers):
                column_label = labels.get((input_key, index), header)
                worksheet.from_list(
                    index,
                    [row[index] if index < len(row) else None for row in rows],
                    lname=column_label,
                )
        band_legend = None
        if plot_type in COMPOSITION_PLOT_TYPES:
            graph = _build_recipe_graph(op, plot_spec, worksheets, resolved_series)
            layer = graph[0]
            origin_plots = []
        elif plot_type == "band":
            graph, layer, origin_plots, band_legend = _build_band_graph(
                op, worksheets, series_specs, resolved_series, plot_spec.get("canvas")
            )
        elif plot_type == "heatmap":
            import numpy as np

            resolved = resolved_series[0]
            _, rows = input_data[resolved["input"]]
            matrix, xy_map = _heatmap_matrix(rows, resolved)
            matrix_sheet = op.new_sheet("m")
            matrix_sheet.from_np(np.array(matrix, dtype=float))
            matrix_sheet.xymap = xy_map
            matrix_sheet.set_label(
                0, _axis_title(plot_spec.get("z_axis"), str(series_specs[0]["z"]))
            )
            graph = _new_graph(op, "heatmap", plot_spec.get("canvas"))
            layer = graph[0]
            origin_plots = [layer.add_mplot(matrix_sheet, 0, type=105)]
        elif plot_type in STACKED_PLOT_TYPES:
            template, origin_plot_type = PLOT_CONFIG[plot_type]
            graph, layer, origin_plots = _build_stacked_graph(
                op,
                input_data,
                series_specs,
                resolved_series,
                template,
                origin_plot_type,
                plot_spec.get("canvas"),
            )
        elif plot_type in OFFSET_STACKED_PLOT_TYPES:
            graph, layer, origin_plots = _build_offset_stacked_line_graph(
                op,
                input_data,
                series_specs,
                resolved_series,
                plot_spec["stack_gap_percent"],
                plot_spec.get("canvas"),
            )
        elif plot_type == "violin":
            graph, layer, origin_plots = _build_violin_graph(
                op,
                input_data,
                series_specs,
                resolved_series,
                plot_spec.get("canvas"),
            )
        else:
            template, origin_plot_type = PLOT_CONFIG[plot_type]
            graph = _new_graph(op, template, plot_spec.get("canvas"))
            layer = graph[0]
            origin_plots = []
            for series_spec, resolved in zip(series_specs, resolved_series, strict=True):
                arguments = {
                    "coly": resolved["y"],
                    "type": origin_plot_type,
                }
                if "x" in resolved:
                    arguments["colx"] = resolved["x"]
                if plot_type in XYZ_PLOT_TYPES:
                    arguments["colz"] = resolved["z"]
                if plot_type == "y_error":
                    arguments["colyerr"] = resolved["y_error"]
                origin_plot = layer.add_plot(worksheets[resolved["input"]], **arguments)
                if plot_type == "bubble":
                    _configure_bubble_plot(op, origin_plot, series_spec, resolved)
                origin_plots.append(origin_plot)
            if plot_type == "grouped_column":
                layer.group()
        if plot_type not in COMPOSITION_PLOT_TYPES:
            if plot_type not in {"polar", "pie"}:
                axes = [
                    ("x", plot_spec.get("x_axis")),
                    ("y", plot_spec.get("y_axis")),
                ]
                if plot_type == "surface_3d":
                    axes.append(("z", plot_spec.get("z_axis")))
                _rescale_with_axis_scales(layer, axes)
                _apply_axis(
                    layer,
                    "x",
                    plot_spec.get("x_axis"),
                    str(series_specs[0].get("x") or "X"),
                )
                _apply_axis(
                    layer,
                    "y",
                    plot_spec.get("y_axis"),
                    str(series_specs[0].get("y") or "Y"),
                )
                if plot_type == "violin":
                    x_title = layer.label("xb")
                    if x_title is not None:
                        x_title.show = True
            else:
                layer.rescale()
            if plot_type == "surface_3d":
                _apply_axis(
                    layer,
                    "z",
                    plot_spec.get("z_axis"),
                    str(series_specs[0].get("z") or "Z"),
                )
            if plot_type != "violin":
                for origin_plot, series_spec in zip(
                    origin_plots, series_specs, strict=True
                ):
                    style = series_spec.get("style")
                    if plot_type == "bubble" and style:
                        style = {
                            key: value for key, value in style.items()
                            if key != "symbol_size"
                        }
                    _apply_series_style(origin_plot, style)
            _apply_annotations(layer, plot_spec.get("annotations"))
            if plot_type in STACKED_PLOT_TYPES | OFFSET_STACKED_PLOT_TYPES:
                _replace_series_legend(layer, series_specs)
            if plot_type == "surface_3d":
                _apply_surface_3d_presentation(
                    layer,
                    plot_spec.get("legend"),
                    x_title=_axis_title(
                        plot_spec.get("x_axis"),
                        str(series_specs[0].get("x") or "X"),
                    ),
                    y_title=_axis_title(
                        plot_spec.get("y_axis"),
                        str(series_specs[0].get("y") or "Y"),
                    ),
                    z_title=_axis_title(
                        plot_spec.get("z_axis"),
                        str(series_specs[0].get("z") or "Z"),
                    ),
                )
            else:
                legend_spec = plot_spec.get("legend")
                if (
                    plot_type in OFFSET_STACKED_PLOT_TYPES or plot_type == "violin"
                ) and legend_spec is None:
                    legend_spec = {"enabled": False}
                _apply_legend(layer, legend_spec)
            if band_legend is not None:
                layer.label("Legend").text = band_legend
            _apply_title(
                layer,
                plot_spec.get("title"),
                plot_spec.get("title_style"),
                graph=graph,
            )
        requested_outputs = request["outputs"]
        formats = list(dict.fromkeys([
            *[item["format"] for item in requested_outputs],
            "opju",
            "png",
        ]))
        if any(item not in FORMATS for item in formats):
            raise ValueError("OUTPUT_FORMAT_UNSUPPORTED")
        artifacts: list[dict[str, Any]] = []
        project = output / "project.opju"
        op.save(str(project))
        _validate_artifact(project, "opju")
        artifacts.append(_manifest(project, "application/x-origin-project"))
        media = {"png": "image/png", "svg": "image/svg+xml", "pdf": "application/pdf"}
        png_output = next((item for item in requested_outputs if item["format"] == "png"), None)
        dpi = (png_output.get("options") or {}).get("dpi", 300) if png_output else 300
        if not isinstance(dpi, int) or isinstance(dpi, bool) or not 72 <= dpi <= 1200:
            raise ValueError("OUTPUT_DPI_INVALID")
        canvas_width_mm = (plot_spec.get("canvas") or {}).get("width_mm", 160)
        pixel_width = round(dpi * canvas_width_mm / 25.4)
        if "svg" in formats:
            target = output / "figure.svg"
            _configure_origin_session(op)
            exported = Path(
                graph.save_fig(
                    str(target),
                    type="svg",
                    width=0,
                    ratio=100,
                )
            ).resolve()
            if exported != target.resolve() or not target.is_file():
                raise RuntimeError("SVG_EXPORT_FAILED")
            normalized_svg, _ = _strip_origin_svg_text_baselines(target.read_bytes())
            target.write_bytes(normalized_svg)
            _validate_artifact(target, "svg")
            artifacts.append(_manifest(target, media["svg"]))

        # Origin's native PNG rasterizer draws a separate printing-baseline
        # primitive under every text object. Export one PDF, remove those vector
        # primitives, then rasterize the normalized page at the requested DPI.
        if "pdf" in formats or "png" in formats:
            pdf_target = output / (
                "figure.pdf" if "pdf" in formats else ".figure-png-source.pdf"
            )
            _configure_origin_session(op)
            exported = Path(
                graph.save_fig(str(pdf_target), type="pdf", width=0, ratio=100)
            ).resolve()
            if exported != pdf_target.resolve() or not pdf_target.is_file():
                raise RuntimeError("PDF_EXPORT_FAILED")
            _strip_origin_pdf_text_baselines(pdf_target)
            _validate_artifact(pdf_target, "pdf")
            if "png" in formats:
                png_target = output / "figure.png"
                rendered_width, _ = _render_pdf_to_png(
                    pdf_target, png_target, pixel_width
                )
                if abs(rendered_width - pixel_width) > 1:
                    raise RuntimeError("PNG_RENDER_WIDTH_INVALID")
                _validate_artifact(png_target, "png")
                artifacts.append(_manifest(png_target, media["png"]))
            if "pdf" in formats:
                artifacts.append(_manifest(pdf_target, media["pdf"]))
            else:
                pdf_target.unlink()
        try:
            originpro_version = version("originpro")
        except PackageNotFoundError:
            originpro_version = "embedded"
        provenance = {
            "adapter_version": ADAPTER_VERSION,
            "originpro_version": originpro_version,
            "request_digest": request_record["request_digest"],
            "inputs": [
                {
                    "key": item["key"],
                    "filename": input_files[item["key"]].name,
                    "sha256": _file_sha256(input_files[item["key"]]),
                }
                for item in input_specs
            ],
            "outputs": requested_outputs,
            "requested_dpi": dpi if png_output else None,
            "png_pixel_width": pixel_width if png_output else None,
            "canvas_mm": plot_spec.get("canvas"),
        }
        plot_spec_path = output / "plot-spec.json"
        provenance_path = output / "provenance.json"
        _atomic_json(plot_spec_path, request)
        _atomic_json(provenance_path, provenance)
        artifacts.append(_manifest(plot_spec_path, "application/json"))
        artifacts.append(_manifest(provenance_path, "application/json"))
        return artifacts
    finally:
        if op.oext:
            op.exit()


def main() -> int:
    if sys.argv[1:] == ["--probe"]:
        return _probe()
    if len(sys.argv) != 2:
        print("[ERR] Usage: worker.py <job-directory>", file=sys.stderr)
        return 2
    job_dir = Path(sys.argv[1])
    request_record: dict[str, Any] = {}
    try:
        request_record = json.loads(
            (job_dir / "request" / "request.json").read_text(encoding="utf-8")
        )
        artifacts = run(job_dir)
        terminal = {
            "job_id": request_record["job_id"],
            "lease_id": request_record["lease_id"],
            "request_digest": request_record["request_digest"],
            "status": "succeeded",
            "error": {},
            "artifact_manifest": artifacts,
            "terminal_at": datetime.now(timezone.utc).isoformat(),
        }
        _atomic_json(job_dir / "artifacts.json", artifacts)
        _atomic_json(job_dir / "terminal.json", terminal)
        print("[OK] Origin job completed.")
        return 0
    except Exception as exception:
        terminal = {
            "job_id": request_record.get("job_id", ""),
            "lease_id": request_record.get("lease_id", ""),
            "request_digest": request_record.get("request_digest", ""),
            "status": "failed",
            "error": {"code": type(exception).__name__, "detail": str(exception)[:500]},
            "artifact_manifest": [],
            "terminal_at": datetime.now(timezone.utc).isoformat(),
        }
        _atomic_json(job_dir / "terminal.json", terminal)
        print(f"[ERR] {type(exception).__name__}: {exception}", file=sys.stderr)
        return 1


if __name__ == "__main__":
    raise SystemExit(main())
