from __future__ import annotations

import hashlib
import os
from io import BytesIO
import re
import subprocess
import sys
import tempfile
import threading
import uuid
from datetime import datetime
from pathlib import Path
import tkinter as tk
from tkinter import filedialog, messagebox, ttk
from urllib.parse import quote, urlparse, urlsplit, urlunsplit
from urllib.request import Request

import openpyxl
from openpyxl.drawing.image import Image as XLImage
from PIL import Image, ImageTk

from app_core.paths import DATA_DIR, DB_PATH
from app_core.http_client import open_url
from app_core.server_api import fetch_app_setting, save_app_setting
from app_core.ui_helpers import REMOTE_IMAGE_CACHE_DIR, configure_striped_treeview, create_contained_photo
from database import get_app_setting, normalize_style_identifier, set_app_setting

SAMPLES_SORT_KEY_OPTIONS = [
    ("created_at", "create_time_column"),
    ("request_number", "samples_request_column"),
    ("buyer", "buyer_name"),
    ("style", "samples_style_column"),
    ("season", "tech_pack_season_column"),
    ("deadline", "samples_deadline_column"),
    ("updated_at", "revised_time_column"),
]

SAMPLES_FILTER_FIELD_OPTIONS = [
    ("__all__", "samples_filter_all_option"),
    ("request_number", "samples_request_column"),
    ("buyer", "buyer_name"),
    ("style", "samples_style_column"),
    ("season", "tech_pack_season_column"),
    ("description", "samples_description_column"),
    ("mr_pic", "samples_mr_pic_column"),
    ("pattern_pic", "samples_pattern_pic_column"),
    ("sewer_pic", "samples_sewer_pic_column"),
    ("ie_pic", "samples_ie_pic_column"),
    ("sample_type", "samples_type_column"),
    ("color", "samples_color_column"),
    ("size", "samples_size_column"),
    ("quantity", "samples_quantity_column"),
    ("fit_evals_status", "samples_fit_status_column"),
    ("remark", "samples_remark_column"),
    ("created_at", "create_time_column"),
    ("material_date", "samples_material_date_column"),
    ("deadline", "samples_deadline_column"),
    ("cutting_date", "samples_cutting_date_column"),
    ("sewing_date", "samples_sewing_date_column"),
    ("finishing_date", "samples_finishing_date_column"),
    ("sending_date", "samples_sending_date_column"),
    ("upload_comment_date", "samples_upload_comment_date_column"),
    ("buyer_comment_date", "samples_buyer_comment_date_column"),
    ("updated_at", "revised_time_column"),
]

SAMPLES_DATE_FILTER_FIELDS = {
    "created_at",
    "material_date",
    "deadline",
    "cutting_date",
    "sewing_date",
    "finishing_date",
    "sending_date",
    "upload_comment_date",
    "buyer_comment_date",
    "updated_at",
}

SAMPLES_COLUMNS = (
    "created_at",
    "request_number",
    "buyer",
    "style",
    "baby_styles",
    "season",
    "image",
    "description",
    "mr_pic",
    "pattern_pic",
    "sewer_pic",
    "ie_pic",
    "material_date",
    "deadline",
    "sample_type",
    "color",
    "size",
    "quantity",
    "cutting_date",
    "sewing_date",
    "finishing_date",
    "sending_date",
    "fit_evals_status",
    "upload_comment_date",
    "buyer_comment_date",
    "updated_at",
    "latest_user",
    "remark",
)

SAMPLES_TREE_COLUMNS = (
    "select_flag",
    "created_at",
    "request_number",
    "buyer",
    "style",
    "season",
    "description",
    "mr_pic",
    "pattern_pic",
    "sewer_pic",
    "ie_pic",
    "material_date",
    "deadline",
    "sample_type",
    "color",
    "size",
    "quantity",
    "cutting_date",
    "sewing_date",
    "finishing_date",
    "sending_date",
    "fit_evals_status",
    "upload_comment_date",
    "buyer_comment_date",
    "updated_at",
    "latest_user",
    "remark",
)

SAMPLE_REPORT_MONTH_NAMES = {
    "JAN": 1,
    "JANUARY": 1,
    "FEB": 2,
    "FEBRUARY": 2,
    "MAR": 3,
    "MARCH": 3,
    "APR": 4,
    "APRIL": 4,
    "MAY": 5,
    "JUN": 6,
    "JUNE": 6,
    "JUL": 7,
    "JULY": 7,
    "AUG": 8,
    "AUGUST": 8,
    "SEP": 9,
    "SEPT": 9,
    "SEPTEMBER": 9,
    "OCT": 10,
    "OCTOBER": 10,
    "NOV": 11,
    "NOVEMBER": 11,
    "DEC": 12,
    "DECEMBER": 12,
}

SAMPLES_FILTER_DEBOUNCE_MS = 180
SAMPLES_TREE_THUMBNAIL_LIMIT = 60
SAMPLE_COVER_PAGE_ASSETS_KEY = "sample_cover_page_assets"


def _resolve_runtime_asset(relative_path: str) -> Path:
    if getattr(sys, "frozen", False):
        base_dir = Path(getattr(sys, "_MEIPASS", Path(sys.executable).resolve().parent))
    else:
        base_dir = Path(__file__).resolve().parent.parent
    return base_dir / relative_path


def _apply_window_icon(window: tk.Misc) -> None:
    try:
        icon_png = _resolve_runtime_asset("assets/sunny_erp_app.png")
        if icon_png.exists():
            icon_image = tk.PhotoImage(file=str(icon_png))
            setattr(window, "_sunny_erp_icon", icon_image)
            window.iconphoto(True, icon_image)
    except Exception:
        pass
    if sys.platform == "win32":
        try:
            icon_ico = _resolve_runtime_asset("assets/sunny_erp_app.ico")
            if icon_ico.exists():
                window.iconbitmap(str(icon_ico))
        except Exception:
            pass


def _build_samples_module(self, parent: ttk.Frame) -> None:
    parent.columnconfigure(0, weight=1)
    parent.rowconfigure(3, weight=1)

    toolbar = ttk.Frame(parent)
    toolbar.grid(row=0, column=0, sticky="ew")
    self.widget_text["samples_import_button"] = ttk.Button(toolbar, command=self.open_new_sample_dialog)
    self.widget_text["samples_import_button"].pack(side="left")
    self.register_permission_widget("action.samples.new", self.widget_text["samples_import_button"])
    self.widget_text["samples_copy_button"] = ttk.Button(toolbar, text="Copy", command=self.copy_selected_sample)
    self.widget_text["samples_copy_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.samples.copy", self.widget_text["samples_copy_button"])
    self.widget_text["samples_import_folder_button"] = ttk.Button(toolbar, command=self.import_samples_report)
    self.widget_text["samples_import_folder_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.samples.import_report", self.widget_text["samples_import_folder_button"])
    self.widget_text["samples_export_button"] = ttk.Button(toolbar, command=lambda: self.export_report("samples"))
    self.widget_text["samples_export_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.samples.export_report", self.widget_text["samples_export_button"])
    self.widget_text["samples_delete_button"] = ttk.Button(toolbar, text="Delete Sample", command=self.delete_selected_samples)
    self.widget_text["samples_delete_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.samples.delete", self.widget_text["samples_delete_button"])
    self.widget_text["samples_edit_button"] = ttk.Button(toolbar, command=self.edit_selected_sample)
    self.widget_text["samples_edit_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.samples.edit", self.widget_text["samples_edit_button"])
    self.widget_text["samples_sync_tech_pack_button"] = ttk.Button(toolbar, command=self.sync_tech_pack_from_samples)
    self.widget_text["samples_sync_tech_pack_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.tool.download_tech_pack", self.widget_text["samples_sync_tech_pack_button"])
    self.widget_text["samples_cover_page_button"] = ttk.Button(toolbar, text="Cover page", command=self.export_sample_cover_page)
    self.widget_text["samples_cover_page_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.samples.export_report", self.widget_text["samples_cover_page_button"])
    self.widget_text["samples_print_button"] = ttk.Button(toolbar, text="Print", command=self.print_sample_cover_page)
    self.widget_text["samples_print_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.samples.export_report", self.widget_text["samples_print_button"])

    meta = ttk.Frame(parent)
    meta.grid(row=1, column=0, sticky="ew", pady=(12, 14))
    meta.columnconfigure(8, weight=1)
    self.widget_text["samples_filter_field_label"] = ttk.Label(meta)
    self.widget_text["samples_filter_field_label"].grid(row=0, column=0, sticky="w", padx=(0, 6))
    self.samples_filter_field_combo = ttk.Combobox(meta, textvariable=self.samples_filter_field_var, state="readonly", width=22)
    self.samples_filter_field_combo.grid(row=0, column=1, sticky="w")
    self.samples_filter_field_combo.bind("<<ComboboxSelected>>", self.on_samples_filter_field_changed)
    self.widget_text["samples_filter_value_label"] = ttk.Label(meta)
    self.widget_text["samples_filter_value_label"].grid(row=0, column=2, sticky="w", padx=(12, 6))
    self.samples_search_entry = ttk.Entry(meta, textvariable=self.samples_search_var, width=36)
    self.samples_search_entry.grid(row=0, column=3, sticky="w")
    self.samples_search_entry.bind("<Return>", self.on_samples_search_submit)
    self.samples_search_entry.bind("<KP_Enter>", self.on_samples_search_submit)
    self.widget_text["samples_date_from_label"] = ttk.Label(meta)
    self.widget_text["samples_date_from_label"].grid(row=0, column=4, sticky="w", padx=(12, 6))
    self.samples_date_from_entry = ttk.Entry(meta, textvariable=self.samples_date_from_var, width=12)
    self.samples_date_from_entry.grid(row=0, column=5, sticky="w", padx=(0, 12))
    self.bind_date_entry_picker(self.samples_date_from_entry, self.samples_date_from_var)
    self.samples_date_from_var.trace_add("write", lambda *_args: self.apply_samples_filter())
    self.widget_text["samples_date_to_label"] = ttk.Label(meta)
    self.widget_text["samples_date_to_label"].grid(row=0, column=6, sticky="w", padx=(0, 6))
    self.samples_date_to_entry = ttk.Entry(meta, textvariable=self.samples_date_to_var, width=12)
    self.samples_date_to_entry.grid(row=0, column=7, sticky="w")
    self.bind_date_entry_picker(self.samples_date_to_entry, self.samples_date_to_var)
    self.samples_date_to_var.trace_add("write", lambda *_args: self.apply_samples_filter())
    self.widget_text["samples_sort_by_label"] = ttk.Label(meta)
    self.widget_text["samples_sort_by_label"].grid(row=1, column=0, sticky="w", pady=(8, 0), padx=(0, 6))
    self.samples_sort_key_combo = ttk.Combobox(meta, textvariable=self.samples_sort_key_var, state="readonly", width=20)
    self.samples_sort_key_combo.grid(row=1, column=1, sticky="w", pady=(8, 0))
    self.samples_sort_key_combo.bind("<<ComboboxSelected>>", self.on_samples_sort_key_changed)
    self.widget_text["samples_sort_order_label"] = ttk.Label(meta)
    self.widget_text["samples_sort_order_label"].grid(row=1, column=2, sticky="w", pady=(8, 0), padx=(12, 6))
    self.samples_sort_order_combo = ttk.Combobox(meta, textvariable=self.samples_sort_order_var, state="readonly", width=12)
    self.samples_sort_order_combo.grid(row=1, column=3, sticky="w", pady=(8, 0))
    self.samples_sort_order_combo.bind("<<ComboboxSelected>>", self.on_samples_sort_order_changed)
    self.update_samples_filter_input_state()

    self.widget_text["samples_styles_section"] = ttk.Label(parent, style="Section.TLabel")
    self.widget_text["samples_styles_section"].grid(row=2, column=0, sticky="nw", pady=(0, 8))

    table_frame = ttk.Frame(parent)
    table_frame.grid(row=3, column=0, sticky="nsew")
    table_frame.columnconfigure(0, weight=1)
    table_frame.rowconfigure(0, weight=1)

    self.samples_tree = ttk.Treeview(table_frame, columns=SAMPLES_TREE_COLUMNS, show="tree headings", selectmode="browse")
    sample_column_widths = {
        "select_flag": 60,
        "created_at": 110,
        "request_number": 90,
        "buyer": 90,
        "style": 90,
        "season": 90,
        "description": 150,
        "mr_pic": 80,
        "pattern_pic": 90,
        "sewer_pic": 80,
        "ie_pic": 80,
        "material_date": 95,
        "deadline": 95,
        "sample_type": 100,
        "color": 95,
        "size": 70,
        "quantity": 85,
        "cutting_date": 95,
        "sewing_date": 95,
        "finishing_date": 95,
        "sending_date": 95,
        "fit_evals_status": 95,
        "upload_comment_date": 105,
        "buyer_comment_date": 105,
        "updated_at": 105,
        "latest_user": 140,
        "remark": 150,
    }
    self.samples_tree.configure(style="Costing.Treeview")
    self.samples_tree.column("#0", width=96, minwidth=96, anchor="center", stretch=False)
    self.samples_tree.heading("#0", text=self.t("samples_image_column"))
    for column_key in SAMPLES_TREE_COLUMNS:
        anchor = "center"
        if column_key in {"request_number", "buyer", "style", "season", "description", "sample_type", "color", "latest_user", "remark"}:
            anchor = "w"
        self.samples_tree.column(column_key, width=sample_column_widths[column_key], anchor=anchor, stretch=False)
        self.samples_tree.heading(column_key, command=lambda key=column_key: self.on_samples_heading_click(key))
    self.samples_tree.heading("latest_user", text=self.t("latest_user_column"), command=lambda: self.on_samples_heading_click("latest_user"))
    configure_striped_treeview(self.samples_tree)
    samples_scroll = ttk.Scrollbar(table_frame, orient="vertical", command=self.samples_tree.yview)
    samples_scroll_x = ttk.Scrollbar(table_frame, orient="horizontal", command=self.samples_tree.xview)
    self.samples_tree.configure(yscrollcommand=samples_scroll.set, xscrollcommand=samples_scroll_x.set)
    self.samples_tree.grid(row=0, column=0, sticky="nsew")
    samples_scroll.grid(row=0, column=1, sticky="ns")
    samples_scroll_x.grid(row=1, column=0, sticky="ew")
    self.samples_tree.bind("<Button-1>", self.on_samples_tree_click)
    self.samples_tree.bind("<Double-Button-1>", self.on_samples_tree_double_click)
    self.samples_status_label = ttk.Label(
        parent,
        textvariable=self.status_var,
        relief="sunken",
        anchor="w",
        justify="left",
        padding=8,
        wraplength=1180,
    )
    self.samples_status_label.grid(row=4, column=0, sticky="ew", pady=(12, 0))


def handle_samples_placeholder_action(self, action_name: str) -> None:
    self.set_status(f"{self.t('samples_placeholder_action')} ({action_name})")


def refresh_samples_module(self) -> None:
    loaded_rows = self.load_sample_records_from_source()
    self.sample_rows = self.enrich_sample_rows(loaded_rows)
    self.apply_samples_filter()
    self.update_samples_module_status()


def enrich_sample_rows(self, rows: list[dict]) -> list[dict]:
    active_prefixes = self.get_active_collection_prefixes() if hasattr(self, "get_active_collection_prefixes") else set()
    enriched_rows: list[dict] = []
    for row in rows:
        enriched = dict(row)
        style_value = enriched.get("style_code") or enriched.get("style")
        enriched["buyer"] = self.get_style_buyer(style_value, active_prefixes) if hasattr(self, "get_style_buyer") else ""
        enriched["season"] = self.get_style_season(style_value) if hasattr(self, "get_style_season") else ""
        enriched["latest_user"] = self.get_entity_latest_user_display("samples", enriched.get("record_id", ""))
        enriched_rows.append(enriched)
    return enriched_rows


def apply_samples_filter(self) -> None:
    if getattr(self, "_samples_filter_update_suspended", False):
        return
    query = str(getattr(self, "samples_search_committed_value", self.samples_search_var.get()) or "").strip().lower()
    from_text = self.samples_date_from_var.get().strip()
    to_text = self.samples_date_to_var.get().strip()
    filter_field = getattr(self, "samples_filter_field_internal", "request_number")
    is_date_field = filter_field in SAMPLES_DATE_FILTER_FIELDS
    self.filtered_sample_rows = list(self.sample_rows)

    if is_date_field:
        self.filtered_sample_rows = [
            row
            for row in self.filtered_sample_rows
            if self.is_within_date_range(row.get(filter_field), from_text, to_text)
        ]
    elif filter_field == "__all__" and query:
        self.filtered_sample_rows = [
            row
            for row in self.filtered_sample_rows
            if query in " ".join(self.display_value(row.get(column_key)) for column_key in SAMPLES_COLUMNS).lower()
        ]
    elif query:
        self.filtered_sample_rows = [
            row
            for row in self.filtered_sample_rows
            if self.display_value(row.get(filter_field)).strip().lower() == query
        ]

    sort_key = self.samples_sort_key_internal
    reverse = self.samples_sort_order_internal == "desc"

    def sort_value(row: dict) -> object:
        value = row.get(sort_key)
        if sort_key in {"created_at", "updated_at"}:
            return self.parse_sort_value(sort_key, str(value or ""))
        return str(value or "").lower()

    self.filtered_sample_rows.sort(key=sort_value, reverse=reverse)
    if hasattr(self, "samples_tree"):
        self.refresh_samples_tree()
    self.update_samples_module_status()


def commit_samples_search_query(self, value: object | None = None, *, apply_filter: bool = True) -> None:
    committed_value = self.samples_search_var.get() if value is None else value
    self.samples_search_committed_value = str(committed_value or "").strip()
    if apply_filter:
        self.apply_samples_filter()


def on_samples_search_submit(self, _event: object | None = None) -> str:
    self.commit_samples_search_query()
    return "break"


def sync_samples_search_query(self, value: object) -> None:
    normalized_value = str(value or "").strip()
    self.samples_search_var.set(normalized_value)
    self.samples_search_committed_value = normalized_value


def schedule_samples_filter_apply(self) -> None:
    self.commit_samples_search_query()


def run_samples_filter_apply(self) -> None:
    self.apply_samples_filter()


def update_samples_filter_input_state(self, *, clear_irrelevant: bool = False) -> None:
    filter_field = getattr(self, "samples_filter_field_internal", "request_number")
    is_date_field = filter_field in SAMPLES_DATE_FILTER_FIELDS
    value_state = "disabled" if is_date_field else "normal"
    date_state = "normal" if is_date_field else "disabled"
    if hasattr(self, "samples_search_entry"):
        self.samples_search_entry.configure(state=value_state)
    if hasattr(self, "samples_date_from_entry"):
        self.samples_date_from_entry.configure(state=date_state)
    if hasattr(self, "samples_date_to_entry"):
        self.samples_date_to_entry.configure(state=date_state)
    if clear_irrelevant:
        self._samples_filter_update_suspended = True
        try:
            if is_date_field:
                self.sync_samples_search_query("")
            else:
                self.samples_date_from_var.set("")
                self.samples_date_to_var.set("")
        finally:
            self._samples_filter_update_suspended = False


def refresh_samples_tree(self, selected_id: str | None = None) -> None:
    current_selection = self.samples_tree.selection() if hasattr(self, "samples_tree") else ()
    if selected_id is None and current_selection:
        selected_id = current_selection[0]
    for item_id in self.samples_tree.get_children():
        self.samples_tree.delete(item_id)
    self.sample_thumbnail_photos = []
    thumbnail_limit = self.get_startup_thumbnail_render_limit("samples") if hasattr(self, "get_startup_thumbnail_render_limit") else None
    if thumbnail_limit is None:
        thumbnail_limit = SAMPLES_TREE_THUMBNAIL_LIMIT

    for index, row in enumerate(self.filtered_sample_rows, start=1):
        row_values: list[str] = []
        thumbnail_photo = None
        if thumbnail_limit is None or index <= thumbnail_limit:
            thumbnail_photo = self.create_sample_thumbnail(row)
            if thumbnail_photo is not None:
                self.sample_thumbnail_photos.append(thumbnail_photo)
        for column_key in SAMPLES_TREE_COLUMNS:
            if column_key == "select_flag":
                value = "[x]" if str(row.get("record_id", "")).strip() in self.checked_sample_ids else "[ ]"
            else:
                value = row.get(column_key)
            row_values.append(self.display_value(value))
        insert_kwargs = {
            "text": "",
            "values": tuple(row_values),
            "tags": ("odd_row" if index % 2 == 1 else "even_row",),
        }
        if thumbnail_photo is not None:
            insert_kwargs["image"] = thumbnail_photo
        self.samples_tree.insert("", "end", str(index), **insert_kwargs)

    if selected_id and self.samples_tree.exists(selected_id):
        self.samples_tree.selection_set(selected_id)
        self.samples_tree.focus(selected_id)
        self.samples_tree.see(selected_id)
    valid_ids = {str(row.get("record_id", "")).strip() for row in self.sample_rows}
    self.checked_sample_ids.intersection_update(valid_ids)
    if hasattr(self, "note_startup_thumbnail_render_state"):
        self.note_startup_thumbnail_render_state(
            "samples",
            total_rows=len(self.filtered_sample_rows),
            rendered_rows=min(len(self.filtered_sample_rows), thumbnail_limit) if thumbnail_limit is not None else len(self.filtered_sample_rows),
        )
    self.update_samples_module_status()


def update_samples_tree_checkbox_state(self, row_id: str | None = None) -> None:
    if not hasattr(self, "samples_tree"):
        return
    if row_id:
        if not self.samples_tree.exists(row_id):
            return
        try:
            index = int(row_id) - 1
        except (TypeError, ValueError):
            return
        if index < 0 or index >= len(self.filtered_sample_rows):
            return
        record_id = str(self.filtered_sample_rows[index].get("record_id", "")).strip()
        value = "[x]" if record_id in self.checked_sample_ids else "[ ]"
        self.samples_tree.set(row_id, "select_flag", value)
    else:
        for item_id in self.samples_tree.get_children():
            try:
                index = int(item_id) - 1
            except (TypeError, ValueError):
                continue
            if index < 0 or index >= len(self.filtered_sample_rows):
                continue
            record_id = str(self.filtered_sample_rows[index].get("record_id", "")).strip()
            value = "[x]" if record_id in self.checked_sample_ids else "[ ]"
            self.samples_tree.set(item_id, "select_flag", value)
    self.update_samples_module_status()


def get_selected_sample_row(self) -> dict | None:
    if not hasattr(self, "samples_tree"):
        return None
    selection = self.samples_tree.selection()
    if not selection:
        return None
    index = int(selection[0]) - 1
    if index < 0 or index >= len(self.filtered_sample_rows):
        return None
    return self.filtered_sample_rows[index]


def get_target_sample_rows(self) -> list[dict]:
    checked_rows = [
        row
        for row in self.sample_rows
        if str(row.get("record_id", "")).strip() in self.checked_sample_ids
    ]
    if checked_rows:
        return checked_rows
    selected_row = self.get_selected_sample_row()
    return [selected_row] if selected_row is not None else []


def get_target_sample_styles_for_sync(self) -> list[str]:
    styles: list[str] = []
    seen: set[str] = set()
    for row in self.get_target_sample_rows():
        style_code = normalize_style_identifier(row.get("style_code") or row.get("style"))
        if not style_code or style_code in seen:
            continue
        seen.add(style_code)
        styles.append(style_code)
    return styles


def uncheck_sample_rows_for_style(self, style_code: str) -> None:
    normalized_style = normalize_style_identifier(style_code)
    if not normalized_style:
        return
    target_ids = {
        str(row.get("record_id", "")).strip()
        for row in self.sample_rows
        if normalize_style_identifier(row.get("style_code") or row.get("style")) == normalized_style
        and str(row.get("record_id", "")).strip()
    }
    if not target_ids:
        return
    if not any(record_id in self.checked_sample_ids for record_id in target_ids):
        return
    self.checked_sample_ids.difference_update(target_ids)
    if hasattr(self, "samples_tree"):
        self.update_samples_tree_checkbox_state()
    else:
        self.update_samples_module_status()


def get_target_sample_cover_page_context(self) -> tuple[dict, str, list[dict]] | None:
    target_rows = self.get_target_sample_rows()
    if not target_rows:
        return None
    style_map: dict[str, list[dict]] = {}
    for row in target_rows:
        style_code = normalize_style_identifier(row.get("style_code") or row.get("style"))
        if not style_code:
            continue
        style_map.setdefault(style_code, []).append(row)
    if not style_map:
        return None
    if len(style_map) > 1:
        return ({}, "", [])
    style_code, style_rows = next(iter(style_map.items()))
    selected_row = self.get_selected_sample_row()
    if selected_row is not None and normalize_style_identifier(selected_row.get("style_code") or selected_row.get("style")) == style_code:
        return selected_row, style_code, style_rows
    return style_rows[0], style_code, style_rows


def sample_cover_page_collection_info(self, style_code: str) -> tuple[str, str]:
    normalized_style = normalize_style_identifier(style_code)
    if not normalized_style:
        return "", ""
    prefix = normalized_style[:3].upper()
    for row in self.load_collection_registry_from_source() if hasattr(self, "load_collection_registry_from_source") else []:
        if str(row.get("prefix", "")).strip().upper() != prefix:
            continue
        collection = str(row.get("collection", "")).strip()
        gender = str(row.get("gender", "")).strip().lower()
        brand = collection.title() if collection else ""
        gender_label = {
            "lady": "Lady",
            "men": "Men",
            "unisex": "Unisex",
        }.get(gender, gender.title())
        return brand, gender_label
    return "", ""


def sample_cover_page_tech_pack_info(self, style_code: str, sample_row: dict | None = None) -> tuple[str, str, str]:
    tech_pack_row = self.get_tech_pack_row_by_style_code(style_code) if hasattr(self, "get_tech_pack_row_by_style_code") else None
    if tech_pack_row is None:
        self.refresh_tech_pack_module(silent=True)
        tech_pack_row = self.get_tech_pack_row_by_style_code(style_code) if hasattr(self, "get_tech_pack_row_by_style_code") else None
    if tech_pack_row is None:
        return "", "", ""
    overview_data = tech_pack_row.get("overview_data", {}) if isinstance(tech_pack_row.get("overview_data", {}), dict) else {}
    sample_stage = str(tech_pack_row.get("sample_stage", "")).strip()
    if not sample_stage:
        sample_stage = str(overview_data.get("sample_stage", "")).strip()
    if not sample_stage:
        sample_stage = str(overview_data.get("sample_type", "")).strip()
    if not sample_stage and isinstance(sample_row, dict):
        sample_stage = str(sample_row.get("sample_type", "")).strip()
    sections = [section for section in list(tech_pack_row.get("bom_data", [])) if isinstance(section, dict)]
    fabric_component = ""
    fabric_composition = ""
    color_tokens = self.build_tech_pack_bom_color_token_set(tech_pack_row) if hasattr(self, "build_tech_pack_bom_color_token_set") else set()
    fallback_material: tuple[str, str] | None = None
    for section in sections:
        section_name = str(section.get("section", "")).strip()
        if fallback_material is None and section_name.upper() == "MATERIAL":
            for item in list(section.get("items", [])):
                if not isinstance(item, dict):
                    continue
                if hasattr(self, "get_tech_pack_bom_display_values"):
                    display_values = self.get_tech_pack_bom_display_values(section_name, item, color_tokens)
                    fallback_material = (
                        str(display_values.get("component", "")).strip() or str(display_values.get("description", "")).strip(),
                        str(display_values.get("composition", "")).strip(),
                    )
                else:
                    fallback_material = (
                        str(item.get("component", "")).strip() or str(item.get("description", "")).strip(),
                        str(item.get("composition", "")).strip(),
                    )
                break
        for item in list(section.get("items", [])):
            if not isinstance(item, dict):
                continue
            if hasattr(self, "get_tech_pack_bom_display_values"):
                display_values = self.get_tech_pack_bom_display_values(section_name, item, color_tokens)
                component = str(display_values.get("component", "")).strip()
                description = str(display_values.get("description", "")).strip()
                composition = str(display_values.get("composition", "")).strip()
            else:
                component = str(item.get("component", "")).strip()
                description = str(item.get("description", "")).strip()
                composition = str(item.get("composition", "")).strip()
            haystack = " ".join(part for part in (section_name, component, description) if part).upper()
            if "FABRIC" not in haystack:
                continue
            fabric_component = component or description
            fabric_composition = composition
            return sample_stage, fabric_component, fabric_composition
    if fallback_material is not None:
        fabric_component, fabric_composition = fallback_material
    return sample_stage, fabric_component, fabric_composition


def parse_sample_baby_styles(value: object) -> list[str]:
    seen: set[str] = set()
    items: list[str] = []
    for line in str(value or "").splitlines():
        style_code = normalize_style_identifier(line)
        if not style_code or style_code in seen:
            continue
        seen.add(style_code)
        items.append(style_code)
    return items


def format_sample_baby_styles(values: list[str]) -> str:
    return "\n".join(parse_sample_baby_styles("\n".join(values)))


def get_sample_cover_page_baby_styles(self, sample_row: dict | None) -> list[str]:
    if not isinstance(sample_row, dict):
        return []
    return parse_sample_baby_styles(sample_row.get("baby_styles", ""))


def get_sample_cover_page_bom_selection(self, sample_row: dict | None) -> tuple[str, str]:
    if not isinstance(sample_row, dict):
        return "", ""
    return (
        str(sample_row.get("bom_item_name", "")).strip(),
        str(sample_row.get("bom_composition", "")).strip(),
    )


def build_sample_cover_page_items(self, sample_row: dict) -> tuple[list[tuple[str, str, str]], str]:
    colors = [line.strip() for line in str(sample_row.get("color", "")).splitlines() if line.strip()]
    sizes = [line.strip() for line in str(sample_row.get("size", "")).splitlines() if line.strip()]
    quantities = [line.strip() for line in str(sample_row.get("quantity", "")).splitlines() if line.strip()]
    row_count = max(len(colors), len(sizes), len(quantities), 1)
    items: list[tuple[str, str, str]] = []
    numeric_total = 0.0
    saw_numeric = False
    for index in range(row_count):
        color_value = colors[index] if index < len(colors) else ""
        size_value = sizes[index] if index < len(sizes) else ""
        quantity_value = quantities[index] if index < len(quantities) else ""
        items.append((color_value, size_value, quantity_value))
        quantity_match = re.search(r"\d+(?:\.\d+)?", quantity_value.replace(",", ""))
        if quantity_match:
            numeric_total += float(quantity_match.group(0))
            saw_numeric = True
    while len(items) > 1 and not any(items[-1]):
        items.pop()
    total_quantity = ""
    if saw_numeric:
        total_quantity = str(int(numeric_total)) if float(numeric_total).is_integer() else str(numeric_total)
    return items, total_quantity


def build_sample_cover_page_file(self, sample_row: dict, style_code: str, save_path: Path) -> Path:
    assets = self.load_sample_cover_page_assets_from_source()
    template_path = self.resolve_sample_cover_page_asset(str(assets.get("template_url", "")).strip(), fallback_suffix=".xlsx")
    if template_path is None:
        raise FileNotFoundError("Sample Request template was not configured on the server.")

    workbook = openpyxl.load_workbook(template_path)
    worksheet = workbook[workbook.sheetnames[0]]
    try:
        workbook._external_links = []
    except Exception:
        pass
    brand, gender = self.sample_cover_page_collection_info(style_code)
    sample_stage, _fabric_component, _fabric_composition = self.sample_cover_page_tech_pack_info(style_code, sample_row)
    items, total_quantity = self.build_sample_cover_page_items(sample_row)
    baby_styles = self.get_sample_cover_page_baby_styles(sample_row)
    bom_item_name, bom_composition = self.get_sample_cover_page_bom_selection(sample_row)

    created_at_text = str(sample_row.get("created_at", "")).strip()
    deadline_text = str(sample_row.get("deadline", "")).strip()
    if created_at_text:
        created_at_text = created_at_text.replace("T", " ").split("+", 1)[0].strip()
    if deadline_text:
        deadline_text = deadline_text.replace("T", " ").split("+", 1)[0].strip()
    request_on_value = created_at_text
    for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
        try:
            parsed_created_at = datetime.strptime(created_at_text, fmt)
            if fmt == "%Y-%m-%d":
                request_on_value = f"{parsed_created_at.month}/{parsed_created_at.day}/{parsed_created_at.year}"
            else:
                hour_12 = parsed_created_at.hour % 12 or 12
                am_pm = "AM" if parsed_created_at.hour < 12 else "PM"
                request_on_value = (
                    f"{parsed_created_at.month}/{parsed_created_at.day}/{parsed_created_at.year}\n"
                    f"{hour_12}:{parsed_created_at.minute:02d}:{parsed_created_at.second:02d} {am_pm}"
                )
            break
        except Exception:
            continue

    worksheet["B4"] = request_on_value
    worksheet["C4"] = deadline_text
    worksheet["D4"] = brand
    worksheet["F4"] = style_code
    worksheet["G4"] = str(sample_row.get("description", "")).strip()
    worksheet["H4"] = str(sample_row.get("season", "")).strip() or self.get_style_season(style_code)
    worksheet["I4"] = gender

    worksheet["B7"] = sample_stage
    worksheet["C7"] = bom_item_name
    worksheet["D7"] = bom_composition
    worksheet["F7"] = str(sample_row.get("mr_pic", "")).strip()
    worksheet["G7"] = str(sample_row.get("pattern_pic", "")).strip()
    worksheet["H7"] = str(sample_row.get("sewer_pic", "")).strip()
    worksheet["I7"] = str(sample_row.get("ie_pic", "")).strip()

    clear_until_row = max(20, 9 + len(baby_styles) + len(items) + 2)
    for row_number in range(9, clear_until_row + 1):
        for column_number in range(2, 6):
            worksheet.cell(row=row_number, column=column_number, value=None)

    display_row_count = max(len(items), len(baby_styles) + 1, 1)
    for row_offset in range(display_row_count):
        row_number = 9 + row_offset
        if row_offset == 0:
            item_label = style_code
        elif row_offset - 1 < len(baby_styles):
            item_label = baby_styles[row_offset - 1]
        else:
            item_label = None
        worksheet.cell(row=row_number, column=2, value=item_label)

        if row_offset < len(items):
            color_value, size_value, quantity_value = items[row_offset]
        else:
            color_value, size_value, quantity_value = ("", "", "")
        worksheet.cell(row=row_number, column=3, value=color_value or None)
        worksheet.cell(row=row_number, column=4, value=size_value or None)
        worksheet.cell(row=row_number, column=5, value=quantity_value or None)
    worksheet["F9"] = total_quantity
    worksheet["B4"].alignment = openpyxl.styles.Alignment(horizontal="center", vertical="center", wrap_text=True)
    worksheet.row_dimensions[4].height = max(float(worksheet.row_dimensions[4].height or 15), 36)

    sample_image_path = self.resolve_sample_row_image(sample_row)
    resolved_sample_image = Path(sample_image_path) if sample_image_path and Path(sample_image_path).exists() else None
    if resolved_sample_image is None and sample_image_path:
        try:
            from app_core.ui_helpers import resolve_image_source
            resolved_sample_image = resolve_image_source(sample_image_path)
        except Exception:
            resolved_sample_image = None
    if resolved_sample_image is not None:
        with Image.open(resolved_sample_image).convert("RGBA") as style_source:
            style_prepared = style_source.copy()
        target_style_width = 288
        scale_ratio = target_style_width / max(1, style_prepared.width)
        target_style_height = max(1, int(round(style_prepared.height * scale_ratio)))
        style_prepared = style_prepared.resize((target_style_width, target_style_height), Image.Resampling.LANCZOS)
        style_buffer = BytesIO()
        style_prepared.save(style_buffer, format="PNG")
        style_buffer.seek(0)
        style_image = XLImage(style_buffer)
        style_image.width = style_prepared.width
        style_image.height = style_prepared.height
        style_image._image_buffer = style_buffer  # type: ignore[attr-defined]
        worksheet.add_image(style_image, "G9")

    logo_by_buyer = assets.get("logo_by_buyer", {}) if isinstance(assets.get("logo_by_buyer", {}), dict) else {}
    configured_logo = str(logo_by_buyer.get(str(sample_row.get("buyer", "")).strip()) or logo_by_buyer.get("Bealls") or "").strip()
    logo_path = self.resolve_sample_cover_page_asset(configured_logo, fallback_suffix=".png")
    if logo_path is None:
        local_logo_path = Path(r"C:\Users\ADMIN\AppData\Local\Temp\codex-clipboard-75796da8-b167-4e4c-8852-e67f3399e558.png")
        if local_logo_path.exists() and self.should_use_server_data_source():
            uploaded_logo_url = self.upload_shared_file_reference(local_logo_path, bucket="sample-cover-assets")
            if uploaded_logo_url and uploaded_logo_url != str(local_logo_path):
                logo_by_buyer[str(sample_row.get("buyer", "")).strip() or "Bealls"] = uploaded_logo_url
                assets["logo_by_buyer"] = logo_by_buyer
                assets = self.save_sample_cover_page_assets_to_source(assets)
                logo_path = self.resolve_sample_cover_page_asset(uploaded_logo_url, fallback_suffix=".png")
        if logo_path is None and local_logo_path.exists():
            logo_path = local_logo_path
    if logo_path is not None and logo_path.exists():
        merged_ranges = {str(item) for item in worksheet.merged_cells.ranges}
        logo_anchor = "H1" if "H1:I2" in merged_ranges else "I1"
        if logo_anchor == "H1":
            worksheet["H1"] = None
            max_logo_width = min(
                192,
                sample_cover_page_column_width_pixels(worksheet.column_dimensions["H"].width)
                + sample_cover_page_column_width_pixels(worksheet.column_dimensions["I"].width)
                - 6,
            )
        else:
            worksheet["I1"] = None
            max_logo_width = min(
                192,
                sample_cover_page_column_width_pixels(worksheet.column_dimensions["I"].width) - 6,
            )
        max_logo_height = (
            sample_cover_page_row_height_pixels(worksheet.row_dimensions[1].height)
            + sample_cover_page_row_height_pixels(worksheet.row_dimensions[2].height)
            - 6
        )
        with Image.open(logo_path).convert("RGBA") as logo_source:
            logo_prepared = logo_source.copy()
        logo_prepared.thumbnail((max_logo_width, max_logo_height), Image.Resampling.LANCZOS)
        logo_buffer = BytesIO()
        logo_prepared.save(logo_buffer, format="PNG")
        logo_buffer.seek(0)
        logo_image = XLImage(logo_buffer)
        logo_image.width = logo_prepared.width
        logo_image.height = logo_prepared.height
        logo_image._image_buffer = logo_buffer  # type: ignore[attr-defined]
        worksheet.add_image(logo_image, logo_anchor)
        worksheet.row_dimensions[1].height = max(float(worksheet.row_dimensions[1].height or 15), 44)
        worksheet.row_dimensions[2].height = max(float(worksheet.row_dimensions[2].height or 15), 24)

    workbook.save(save_path)
    return save_path


def export_sample_cover_page(self) -> None:
    if not self.is_action_allowed("action.samples.export_report"):
        return
    context = self.get_target_sample_cover_page_context()
    if context is None:
        messagebox.showwarning("Cover page", "Please tick or select one sample style first.")
        return
    sample_row, style_code, style_rows = context
    if not style_code:
        messagebox.showwarning("Cover page", "Please select sample rows from only one style.")
        return

    save_name = f"SAMPLE_REQUEST_{style_code}_{str(sample_row.get('request_number', '')).strip() or datetime.now().strftime('%Y%m%d%H%M')}.xlsx"
    save_path = filedialog.asksaveasfilename(
        parent=self.root,
        title="Save Cover Page",
        defaultextension=".xlsx",
        filetypes=[("Excel files", "*.xlsx")],
        initialfile=save_name,
    )
    if not save_path:
        return
    try:
        self.build_sample_cover_page_file(sample_row, style_code, Path(save_path))
    except FileNotFoundError:
        messagebox.showerror("Cover page", "Sample Request template was not configured on the server.")
        self.set_status("Cover page export failed.")
        return
    except Exception as exc:
        messagebox.showerror("Cover page", f"Could not save cover page.\n\n{exc}")
        self.set_status("Cover page export failed.")
        return

    self.set_status(f"Saved cover page for {style_code}: {Path(save_path).name}")
    messagebox.showinfo("Cover page", f"Saved cover page for {style_code}.")


def convert_sample_cover_page_to_pdf(self, workbook_path: Path, pdf_path: Path) -> Path:
    if sys.platform != "win32":
        raise RuntimeError("PDF conversion is currently supported on Windows only.")
    pdf_path.parent.mkdir(parents=True, exist_ok=True)
    powershell_script = """
param(
    [string]$xlsxPath,
    [string]$pdfPath
)

$ErrorActionPreference = 'Stop'
$excel = $null
$workbook = $null
try {
    $resolvedXlsx = [System.IO.Path]::GetFullPath($xlsxPath)
    $resolvedPdf = [System.IO.Path]::GetFullPath($pdfPath)
    $excel = New-Object -ComObject Excel.Application
    $excel.Visible = $false
    $excel.DisplayAlerts = $false
    $workbook = $excel.Workbooks.Open($resolvedXlsx)
    $xlTypePdf = 0
    $workbook.ExportAsFixedFormat($xlTypePdf, $resolvedPdf)
}
finally {
    if ($workbook -ne $null) {
        $workbook.Close($false)
        [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($workbook)
    }
    if ($excel -ne $null) {
        $excel.Quit()
        [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel)
    }
    [GC]::Collect()
    [GC]::WaitForPendingFinalizers()
}
"""
    script_path_value = ""
    try:
        with tempfile.NamedTemporaryFile("w", suffix="_sample_cover_to_pdf.ps1", delete=False, encoding="utf-8") as handle:
            handle.write(powershell_script.strip())
            script_path_value = handle.name
        startupinfo = None
        creationflags = 0
        if sys.platform == "win32":
            startupinfo = subprocess.STARTUPINFO()
            startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
            creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
        subprocess.run(
            [
                "powershell",
                "-NoProfile",
                "-ExecutionPolicy",
                "Bypass",
                "-File",
                script_path_value,
                str(workbook_path),
                str(pdf_path),
            ],
            check=True,
            capture_output=True,
            text=True,
            startupinfo=startupinfo,
            creationflags=creationflags,
        )
    finally:
        if script_path_value:
            try:
                Path(script_path_value).unlink(missing_ok=True)
            except Exception:
                pass
    if not pdf_path.exists():
        raise RuntimeError("PDF file was not created.")
    return pdf_path


def print_sample_cover_page(self) -> None:
    if not self.is_action_allowed("action.samples.export_report"):
        return
    if sys.platform != "win32":
        messagebox.showinfo("Print Cover Page", "Printing is currently supported on Windows only.")
        return
    context = self.get_target_sample_cover_page_context()
    if context is None:
        messagebox.showwarning("Print Cover Page", "Please tick or select one sample style first.")
        return
    sample_row, style_code, style_rows = context
    if not style_code:
        messagebox.showwarning("Print Cover Page", "Please select sample rows from only one style.")
        return

    temp_dir = DATA_DIR / "sample_cover_page_prints"
    temp_dir.mkdir(parents=True, exist_ok=True)
    timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
    temp_path = temp_dir / f"PRINT_SAMPLE_REQUEST_{style_code}_{timestamp}.xlsx"
    pdf_path = temp_dir / f"PRINT_SAMPLE_REQUEST_{style_code}_{timestamp}.pdf"
    try:
        self.build_sample_cover_page_file(sample_row, style_code, temp_path)
        self.convert_sample_cover_page_to_pdf(temp_path, pdf_path)
        os.startfile(str(pdf_path))  # type: ignore[attr-defined]
    except FileNotFoundError:
        messagebox.showerror("Print Cover Page", "Sample Request template was not configured on the server.")
        self.set_status("Cover page print failed.")
        return
    except subprocess.CalledProcessError as exc:
        detail = (exc.stderr or exc.stdout or str(exc)).strip()
        messagebox.showerror("Print Cover Page", f"Could not create PDF for printing.\n\n{detail}")
        self.set_status("Cover page print failed.")
        return
    except Exception as exc:
        messagebox.showerror("Print Cover Page", f"Could not print cover page.\n\n{exc}")
        self.set_status("Cover page print failed.")
        return

    self.set_status(f"Opened PDF cover page for {style_code}. Please print from the PDF viewer.")


def on_samples_tree_click(self, event: tk.Event) -> str | None:
    region = self.samples_tree.identify_region(event.x, event.y)
    if region != "cell":
        return None
    column = self.samples_tree.identify_column(event.x)
    if column != "#1":
        return None
    row_id = self.samples_tree.identify_row(event.y)
    if not row_id:
        return None
    index = int(row_id) - 1
    if index < 0 or index >= len(self.filtered_sample_rows):
        return "break"
    record_id = str(self.filtered_sample_rows[index].get("record_id", "")).strip()
    if not record_id:
        return "break"
    if record_id in self.checked_sample_ids:
        self.checked_sample_ids.remove(record_id)
    else:
        self.checked_sample_ids.add(record_id)
    self.update_samples_tree_checkbox_state(row_id)
    self.samples_tree.selection_set(row_id)
    self.samples_tree.focus(row_id)
    return "break"


def on_samples_tree_double_click(self, event: tk.Event) -> str | None:
    row_id = self.samples_tree.identify_row(event.y)
    if not row_id:
        return None
    try:
        index = int(row_id) - 1
    except (TypeError, ValueError):
        return "break"
    if index < 0 or index >= len(self.filtered_sample_rows):
        return "break"
    if not self.is_action_allowed("action.samples.edit"):
        return "break"
    self.samples_tree.selection_set(row_id)
    self.samples_tree.focus(row_id)
    self.open_new_sample_dialog(self.filtered_sample_rows[index])
    return "break"


def sample_style_option_rows(self) -> list[dict]:
    options: list[dict] = []
    seen: set[str] = set()
    source_rows = list(self.tech_pack_rows)
    if not source_rows and not getattr(self, "startup_preload_running", False):
        self.refresh_tech_pack_module(silent=True)
        source_rows = list(self.tech_pack_rows)
    for row in source_rows:
        style_code = normalize_style_identifier(row.get("style_code"))
        if not style_code or style_code in seen:
            continue
        seen.add(style_code)
        style_name = str(row.get("style_name", "")).strip()
        label = f"{style_code} - {style_name}" if style_name else style_code
        options.append(
            {
                "style_code": style_code,
                "style_name": style_name,
                "label": label,
                "image": str(row.get("image_path", "")).strip(),
                "description": style_name,
            }
        )
    options.sort(key=lambda item: item["label"].lower())
    return options


def sample_pic_options_by_role(self) -> dict[str, list[str]]:
    shared_lists = self.load_shared_lists_from_source() if hasattr(self, "load_shared_lists_from_source") else {}
    role_options = {
        "mr_pic": list(shared_lists.get("mr", [])),
        "pattern_pic": list(shared_lists.get("pattern", [])),
        "sewer_pic": list(shared_lists.get("sewer", [])),
        "ie_pic": list(shared_lists.get("ie", [])),
    }
    for row in self.sample_rows:
        for key, options in role_options.items():
            value = str(row.get(key, "")).strip()
            if value and value not in options:
                options.append(value)
    for key, options in role_options.items():
        role_options[key] = sorted(options, key=str.lower)
    return role_options


def sample_type_options_list(self) -> list[str]:
    shared_lists = self.load_shared_lists_from_source() if hasattr(self, "load_shared_lists_from_source") else {}
    values = list(shared_lists.get("sample_type", []))
    seen = {value.lower() for value in values}
    for row in self.sample_rows:
        value = str(row.get("sample_type", "")).strip()
        lowered = value.lower()
        if not value or lowered in seen:
            continue
        seen.add(lowered)
        values.append(value)
    return values


def find_sample_style_option(self, style_code: str) -> dict[str, str] | None:
    normalized_style = normalize_style_identifier(style_code)
    if not normalized_style:
        return None
    return next(
        (option for option in self.sample_style_option_rows() if normalize_style_identifier(option.get("style_code")) == normalized_style),
        None,
    )


def resolve_sample_row_image(self, row: dict) -> str:
    style_code = str(row.get("style_code") or row.get("style") or "").strip()
    if getattr(self, "tech_pack_rows", None):
        style_option = self.find_sample_style_option(style_code)
        if style_option is not None:
            current_image = str(style_option.get("image", "")).strip()
            if current_image:
                return current_image
    return str(row.get("image", "")).strip()


def format_sample_image_display(image_path: str) -> str:
    raw_value = str(image_path or "").strip()
    if not raw_value:
        return ""
    lowered = raw_value.lower()
    if lowered.startswith("http://") or lowered.startswith("https://"):
        name = Path(raw_value).name or raw_value.rstrip("/").split("/")[-1]
        return f".../{name}" if name else raw_value
    name = Path(raw_value).name or raw_value
    return f"Pending upload - {name}"


def default_sample_cover_page_assets() -> dict[str, object]:
    return {
        "template_url": "https://texmind.xyz/shared/sample-cover-assets/20260710/c876cf417da64bf697c2bf8379dd6ec7/SAMPLE REQUEST FORM.xlsx",
        "logo_by_buyer": {
            "Bealls": "https://texmind.xyz/shared/sample-cover-assets/20260710/ebdf45d6d6104e8e94683286d8b1c2c2/codex-clipboard-75796da8-b167-4e4c-8852-e67f3399e558.png",
        },
    }


def load_sample_cover_page_assets_from_source(self) -> dict[str, object]:
    default_value = default_sample_cover_page_assets()
    if self.should_use_server_data_source():
        if self.is_local_data_mode():
            self.ensure_local_server_running()
        try:
            payload = fetch_app_setting(self.server_settings, SAMPLE_COVER_PAGE_ASSETS_KEY, default_value)
            merged = dict(default_value)
            if isinstance(payload, dict):
                merged.update(payload)
                logo_map = dict(default_value.get("logo_by_buyer", {}))
                if isinstance(payload.get("logo_by_buyer", {}), dict):
                    logo_map.update(payload.get("logo_by_buyer", {}))
                merged["logo_by_buyer"] = logo_map
            return merged
        except Exception:
            pass
    payload = get_app_setting(DB_PATH, SAMPLE_COVER_PAGE_ASSETS_KEY, default_value)
    merged = dict(default_value)
    if isinstance(payload, dict):
        merged.update(payload)
        logo_map = dict(default_value.get("logo_by_buyer", {}))
        if isinstance(payload.get("logo_by_buyer", {}), dict):
            logo_map.update(payload.get("logo_by_buyer", {}))
        merged["logo_by_buyer"] = logo_map
    return merged


def save_sample_cover_page_assets_to_source(self, payload: dict[str, object]) -> dict[str, object]:
    normalized = dict(default_sample_cover_page_assets())
    normalized.update(payload if isinstance(payload, dict) else {})
    if self.should_use_server_data_source():
        if self.is_local_data_mode():
            self.ensure_local_server_running()
        saved = save_app_setting(self.server_settings, SAMPLE_COVER_PAGE_ASSETS_KEY, normalized)
        return dict(saved) if isinstance(saved, dict) else normalized
    saved = set_app_setting(DB_PATH, SAMPLE_COVER_PAGE_ASSETS_KEY, normalized)
    return dict(saved) if isinstance(saved, dict) else normalized


def resolve_sample_cover_page_asset(self, value: str, *, fallback_suffix: str) -> Path | None:
    raw_value = str(value or "").strip()
    if not raw_value:
        return None
    candidate = Path(raw_value)
    if candidate.exists() and candidate.is_file():
        return candidate
    if raw_value.lower().startswith(("http://", "https://")):
        try:
            split = urlsplit(raw_value)
            normalized_url = urlunsplit(
                (
                    split.scheme,
                    split.netloc,
                    quote(split.path, safe="/:@"),
                    quote(split.query, safe="=&?/:@"),
                    quote(split.fragment, safe="=&?/:@"),
                )
            )
            parsed = urlparse(normalized_url)
            suffix = Path(parsed.path).suffix or fallback_suffix
            cache_dir = DATA_DIR / "sample_cover_page_assets"
            cache_dir.mkdir(parents=True, exist_ok=True)
            target_path = cache_dir / f"{hashlib.sha1(raw_value.encode('utf-8')).hexdigest()}{suffix}"
            if target_path.exists() and target_path.is_file():
                return target_path
            request = Request(normalized_url, headers={"User-Agent": "CitiFoxERPApp/desktop"})
            with open_url(request, timeout=20) as response:
                target_path.write_bytes(response.read())
            return target_path if target_path.exists() else None
        except Exception:
            return None
    return None


def sample_cover_page_column_width_pixels(width: object) -> int:
    numeric_width = float(width or 8.43)
    return max(12, int(round(numeric_width * 7 + 5)))


def sample_cover_page_row_height_pixels(height_points: object) -> int:
    numeric_height = float(height_points or 15.0)
    return max(12, int(round(numeric_height * 96 / 72)))


def format_sample_request_number(prefix: str, sequence_number: int) -> str:
    return f"{prefix}{sequence_number:02d}"


def make_sample_request_number(self, *, for_date: datetime | None = None, existing_rows: list[dict] | None = None) -> str:
    prefix = (for_date or datetime.now()).strftime("%Y%m")
    source_rows = existing_rows if existing_rows is not None else self.sample_rows
    existing_numbers = [str(row.get("request_number", "")).strip() for row in source_rows]
    highest = 0
    for value in existing_numbers:
        if not value.startswith(prefix):
            continue
        suffix = value[len(prefix) :]
        if suffix.isdigit():
            highest = max(highest, int(suffix))
    return format_sample_request_number(prefix, highest + 1)


def normalize_sample_report_text(value: object) -> str:
    if value is None:
        return ""
    if isinstance(value, datetime):
        return value.strftime("%Y-%m-%d")
    text = str(value).replace("\r\n", "\n").replace("\r", "\n").strip()
    return re.sub(r"[ \t]+\n", "\n", text)


def normalize_sample_report_date(value: object) -> str:
    if value is None:
        return ""
    if isinstance(value, datetime):
        return value.strftime("%Y-%m-%d")
    text = normalize_sample_report_text(value)
    if not text:
        return ""
    iso_match = re.search(r"(\d{4})-(\d{1,2})-(\d{1,2})", text)
    if iso_match:
        return f"{int(iso_match.group(1)):04d}-{int(iso_match.group(2)):02d}-{int(iso_match.group(3)):02d}"
    mdy_match = re.search(r"(\d{1,2})/(\d{1,2})/(\d{4})", text)
    if mdy_match:
        return f"{int(mdy_match.group(3)):04d}-{int(mdy_match.group(1)):02d}-{int(mdy_match.group(2)):02d}"
    return text


def normalize_sample_report_header(value: object) -> str:
    return re.sub(r"\s+", " ", normalize_sample_report_text(value)).strip().lower()


def extract_sample_report_month_prefix(sheet_title: str) -> str | None:
    text = re.sub(r"\s+", " ", str(sheet_title or "").strip()).upper()
    matched = re.fullmatch(r"([A-Z]+)\s+(\d{4})", text)
    if not matched:
        return None
    month_number = SAMPLE_REPORT_MONTH_NAMES.get(matched.group(1))
    if month_number is None:
        return None
    return f"{int(matched.group(2)):04d}{month_number:02d}"


def extract_sample_report_style_code(value: object) -> str:
    text = normalize_sample_report_text(value)
    if not text:
        return ""
    first_line = next((line.strip() for line in text.splitlines() if line.strip()), "")
    return first_line


def split_sample_report_lines(value: object) -> list[str]:
    return [line.strip() for line in normalize_sample_report_text(value).splitlines() if line.strip()]


def build_sample_report_role_lookup(role_options: dict[str, list[str]]) -> dict[str, tuple[str, str]]:
    normalized_to_role: dict[str, tuple[str, str]] = {}
    for role_key, options in role_options.items():
        for option in options:
            normalized_option = normalize_style_identifier(option)
            if normalized_option and normalized_option not in normalized_to_role:
                normalized_to_role[normalized_option] = (role_key, option)
    return normalized_to_role


def parse_sample_report_person_in_charge_from_lookup(
    value: object,
    role_lookup: dict[str, tuple[str, str]],
) -> dict[str, str]:
    resolved = {
        "mr_pic": "",
        "pattern_pic": "",
        "sewer_pic": "",
        "ie_pic": "",
    }
    for raw_line in split_sample_report_lines(value):
        candidate = re.sub(r"\s*[=:]?\s*\d+\s*$", "", raw_line).strip()
        normalized_candidate = normalize_style_identifier(candidate)
        if not normalized_candidate:
            continue
        role_info = role_lookup.get(normalized_candidate)
        if role_info is None:
            continue
        role_key, canonical_name = role_info
        if not resolved[role_key]:
            resolved[role_key] = canonical_name
    return resolved


def parse_sample_report_person_in_charge(self, value: object) -> dict[str, str]:
    role_lookup = build_sample_report_role_lookup(self.sample_pic_options_by_role())
    return parse_sample_report_person_in_charge_from_lookup(value, role_lookup)


def parse_sample_report_color_size_quantity(value_color: object, value_size: object, value_quantity: object) -> tuple[str, str, str]:
    color_lines = split_sample_report_lines(value_color)
    size_lines = split_sample_report_lines(value_size)
    raw_quantity = normalize_sample_report_text(value_quantity)
    parsed_colors: list[str] = []
    parsed_quantities: list[str] = []
    for line in color_lines:
        matched = re.fullmatch(r"(.+?)\s*=\s*(\d+(?:\.\d+)?)", line)
        if matched:
            parsed_colors.append(matched.group(1).strip())
            parsed_quantities.append(matched.group(2).rstrip("0").rstrip("."))
        else:
            parsed_colors.append(line)
    if parsed_quantities:
        if not size_lines:
            size_lines = [""] * len(parsed_quantities)
        elif len(size_lines) == 1 and len(parsed_quantities) > 1:
            size_lines = size_lines * len(parsed_quantities)
        quantity_text = "\n".join(parsed_quantities)
    else:
        quantity_text = raw_quantity
    if len(size_lines) == 1 and len(parsed_colors) > 1:
        size_lines = size_lines * len(parsed_colors)
    color_text = "\n".join(parsed_colors)
    size_text = "\n".join(size_lines)
    return color_text, size_text, quantity_text


def build_sample_style_lookup(style_options: list[dict]) -> dict[str, dict[str, str]]:
    lookup: dict[str, dict[str, str]] = {}
    for option in style_options:
        style_code = normalize_style_identifier(option.get("style_code"))
        if style_code and style_code not in lookup:
            lookup[style_code] = {
                "style_code": str(option.get("style_code", "")).strip(),
                "style_name": str(option.get("style_name", "")).strip(),
                "image": str(option.get("image", "")).strip(),
            }
    return lookup


def parse_sample_report_sheet_rows(
    worksheet,
    import_timestamp: str,
    *,
    style_lookup: dict[str, dict[str, str]],
    role_lookup: dict[str, tuple[str, str]],
    default_row_factory,
) -> list[dict]:
    month_prefix = extract_sample_report_month_prefix(worksheet.title)
    if month_prefix is None:
        return []
    headers = [normalize_sample_report_text(cell) for cell in next(worksheet.iter_rows(min_row=1, max_row=1, values_only=True))]
    header_map = {normalize_sample_report_header(value): index for index, value in enumerate(headers) if normalize_sample_report_header(value)}
    rows: list[dict] = []
    for raw_values in worksheet.iter_rows(min_row=2, values_only=True):
        source_row: dict[str, object] = {}
        for header_text, index in header_map.items():
            source_row[headers[index]] = raw_values[index] if index < len(raw_values) else None
        request_no_raw = source_row.get("NO.")
        style_raw = source_row.get("Style#")
        description_raw = source_row.get("Description")
        if request_no_raw in (None, "") and not any(
            normalize_sample_report_text(source_row.get(key))
            for key in ("Style#", "Description", "Person in charge", "Type", "Color")
        ):
            continue
        try:
            request_sequence = int(float(str(request_no_raw).strip()))
        except Exception:
            continue
        style_code_raw = extract_sample_report_style_code(style_raw)
        style_code = normalize_style_identifier(style_code_raw)
        if not style_code:
            continue
        style_option = style_lookup.get(style_code, {})
        person_roles = parse_sample_report_person_in_charge_from_lookup(source_row.get("Person in charge"), role_lookup)
        color_text, size_text, quantity_text = parse_sample_report_color_size_quantity(
            source_row.get("Color"),
            source_row.get("Size"),
            source_row.get("Requested Quantity"),
        )
        description_text = normalize_sample_report_text(description_raw)
        new_row = default_row_factory()
        new_row.update(
            {
                "record_id": str(uuid.uuid4()),
                "created_at": import_timestamp,
                "updated_at": import_timestamp,
                "request_number": format_sample_request_number(month_prefix, request_sequence),
                "style": style_code,
                "style_code": style_code,
                "style_name": str(style_option.get("style_name", "")).strip() or description_text,
                "image": str(style_option.get("image", "")).strip(),
                "description": description_text,
                "mr_pic": person_roles["mr_pic"],
                "pattern_pic": person_roles["pattern_pic"],
                "sewer_pic": person_roles["sewer_pic"],
                "ie_pic": person_roles["ie_pic"],
                "material_date": normalize_sample_report_date(source_row.get("Material Date")),
                "deadline": normalize_sample_report_date(source_row.get("Sample Complete Deadline")),
                "sample_type": normalize_sample_report_text(source_row.get("Type")),
                "color": color_text,
                "size": size_text,
                "quantity": quantity_text,
                "cutting_date": normalize_sample_report_date(source_row.get("Cutting date")),
                "sewing_date": normalize_sample_report_date(source_row.get("Sewing date")),
                "finishing_date": normalize_sample_report_date(source_row.get("Finishing date")),
                "sending_date": normalize_sample_report_date(source_row.get("Sending Date")),
                "fit_evals_status": normalize_sample_report_text(source_row.get("Status")),
                "upload_comment_date": "",
                "buyer_comment_date": "",
                "remark": "",
            }
        )
        rows.append(new_row)
    return rows


def parse_sample_report_sheet(self, worksheet, import_timestamp: str) -> list[dict]:
    return parse_sample_report_sheet_rows(
        worksheet,
        import_timestamp,
        style_lookup=build_sample_style_lookup(self.sample_style_option_rows()),
        role_lookup=build_sample_report_role_lookup(self.sample_pic_options_by_role()),
        default_row_factory=self.default_sample_record,
    )


def set_samples_report_import_running(self, is_running: bool) -> None:
    self.samples_report_import_running = bool(is_running)
    state = "disabled" if is_running else "normal"
    for widget_key in (
        "samples_import_button",
        "samples_copy_button",
        "samples_import_folder_button",
        "samples_delete_button",
        "samples_edit_button",
        "samples_sync_tech_pack_button",
        "samples_cover_page_button",
    ):
        widget = self.widget_text.get(widget_key)
        if widget is None:
            continue
        try:
            widget.configure(state=state)
        except Exception:
            continue
    if not is_running and hasattr(self, "apply_permission_state"):
        try:
            self.apply_permission_state()
        except Exception:
            pass


def rebuild_tech_pack_rows_from_current_state(self) -> None:
    if not hasattr(self, "tech_pack_library"):
        return
    self.tech_pack_rows = self.build_tech_pack_rows()
    if hasattr(self, "tech_pack_tree"):
        self.apply_tech_pack_filter()
    else:
        self.update_tech_pack_module_status()


def _run_samples_report_import_worker(
    self,
    file_path: str,
    import_timestamp: str,
    existing_rows_snapshot: list[dict],
    style_lookup: dict[str, dict[str, str]],
    role_lookup: dict[str, tuple[str, str]],
) -> None:
    result_payload: dict[str, object]
    try:
        workbook = openpyxl.load_workbook(file_path, data_only=True, read_only=True)
        try:
            imported_rows: list[dict] = []
            for worksheet in workbook.worksheets:
                imported_rows.extend(
                    parse_sample_report_sheet_rows(
                        worksheet,
                        import_timestamp,
                        style_lookup=style_lookup,
                        role_lookup=role_lookup,
                        default_row_factory=self.default_sample_record,
                    )
                )
        finally:
            workbook.close()
        if not imported_rows:
            result_payload = {
                "kind": "warning",
                "message": "No valid sample rows were found in the selected workbook.",
            }
        else:
            existing_by_request = {
                str(row.get("request_number", "")).strip(): row
                for row in existing_rows_snapshot
                if str(row.get("request_number", "")).strip()
            }
            merged_rows: list[dict] = []
            imported_request_numbers = {str(row.get("request_number", "")).strip() for row in imported_rows}
            for imported_row in imported_rows:
                existing_row = existing_by_request.get(str(imported_row.get("request_number", "")).strip())
                if existing_row is not None:
                    imported_row["record_id"] = str(existing_row.get("record_id", "")).strip() or str(imported_row.get("record_id", "")).strip()
                merged_rows.append(imported_row)
            merged_rows.extend(
                row
                for row in existing_rows_snapshot
                if str(row.get("request_number", "")).strip() not in imported_request_numbers
            )
            saved_rows = self.save_sample_records_to_source(merged_rows, queue_image_sync=False)
            result_payload = {
                "kind": "success",
                "saved_rows": saved_rows,
                "imported_count": len(imported_rows),
                "file_name": Path(file_path).name,
            }
    except Exception as exc:
        result_payload = {
            "kind": "error",
            "message": f"Could not import sample report.\n\n{exc}",
        }
    self.schedule_root_after(0, lambda payload=result_payload: self.finish_samples_report_import(payload))


def finish_samples_report_import(self, payload: dict[str, object]) -> None:
    self.set_samples_report_import_running(False)
    kind = str(payload.get("kind", "")).strip()
    if kind == "success":
        saved_rows = payload.get("saved_rows", [])
        self.checked_sample_ids.clear()
        self.sample_rows = self.enrich_sample_rows(self.normalize_sample_records(saved_rows))
        self.apply_samples_filter()
        self.rebuild_tech_pack_rows_from_current_state()
        if self.should_use_server_data_source() and self.sample_records_need_image_sync(self.sample_rows):
            self.request_background_image_sync("samples")
        imported_count = int(payload.get("imported_count", 0) or 0)
        file_name = str(payload.get("file_name", "")).strip()
        messagebox.showinfo(
            "Import Report",
            f"Imported {imported_count} sample row(s) from {file_name}.",
        )
        self.set_status(f"Imported {imported_count} sample row(s) from report.")
        return
    if kind == "warning":
        messagebox.showwarning("Import Report", str(payload.get("message", "")).strip())
        self.set_status(str(payload.get("message", "")).strip() or "Import Report finished with no valid rows.")
        return
    messagebox.showerror("Import Report", str(payload.get("message", "")).strip() or "Could not import sample report.")
    self.set_status("Import Report failed.")


def import_samples_report(self) -> None:
    if not self.is_action_allowed("action.samples.import_report"):
        return
    if getattr(self, "samples_report_import_running", False):
        messagebox.showinfo("Import Report", "A sample report import is already running.")
        return
    file_path = filedialog.askopenfilename(
        parent=self.root,
        title="Choose Sample Report",
        filetypes=[("Excel Workbook", "*.xlsx *.xlsm"), ("All Files", "*.*")],
    )
    if not file_path:
        return
    import_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    style_lookup = build_sample_style_lookup(self.sample_style_option_rows())
    role_lookup = build_sample_report_role_lookup(self.sample_pic_options_by_role())
    existing_rows_snapshot = [dict(row) for row in self.sample_rows]
    self.set_samples_report_import_running(True)
    self.set_status(f"Importing sample report in background: {Path(file_path).name}")
    worker = threading.Thread(
        target=self._run_samples_report_import_worker,
        args=(file_path, import_timestamp, existing_rows_snapshot, style_lookup, role_lookup),
        daemon=True,
        name="samples-report-import",
    )
    worker.start()


def create_sample_dialog_preview_photo(self, image_path: str, *, target_width: int = 140, target_height: int = 140) -> ImageTk.PhotoImage | None:
    return create_contained_photo(
        image_path,
        target_width=target_width,
        target_height=target_height,
    )


def create_sample_thumbnail(self, row: dict, target_width: int = 84, target_height: int = 84) -> ImageTk.PhotoImage | None:
    image_path = self.resolve_sample_row_image(row)
    if self.is_remote_image_reference_cached_only(image_path):
        return None
    return create_contained_photo(
        image_path,
        target_width=target_width,
        target_height=target_height,
    )


def is_remote_image_reference_cached_only(self, image_path: str | Path | object) -> bool:
    raw_value = str(image_path or "").strip()
    lowered = raw_value.lower()
    if not lowered.startswith(("http://", "https://")):
        return False
    try:
        parsed = urlparse(raw_value)
        suffix = Path(parsed.path).suffix or ".img"
        cached_path = REMOTE_IMAGE_CACHE_DIR / f"{hashlib.sha1(raw_value.encode('utf-8')).hexdigest()}{suffix}"
        return not cached_path.exists()
    except Exception:
        return True


def sample_size_options(self) -> list[str]:
    return ["XS", "S", "M", "L", "XL", "2XL", "3XL", "1X", "2X", "3X", "PXS", "PS", "PM", "PL", "PXL"]


def extract_tech_pack_colors(self, style_code: str) -> list[str]:
    normalized_style = normalize_style_identifier(style_code)
    if not normalized_style:
        return ["Available"]
    row = next((item for item in self.tech_pack_rows if normalize_style_identifier(item.get("style_code")) == normalized_style), None)
    if row is None:
        self.refresh_tech_pack_module(silent=True)
        row = next((item for item in self.tech_pack_rows if normalize_style_identifier(item.get("style_code")) == normalized_style), None)
    colors: list[str] = []
    overview_data = row.get("overview_data", {}) if isinstance(row, dict) else {}
    if isinstance(overview_data, dict):
        for item in list(overview_data.get("colors", [])):
            if not isinstance(item, dict):
                continue
            for key in ("colorway", "name", "bealls_name", "color_code"):
                value = str(item.get(key, "")).strip()
                if value and value not in colors:
                    colors.append(value)
                    break
    return ["Available", *colors] if colors else ["Available"]


def open_add_sample_baby_style_dialog(
    self,
    lead_style_code: str,
    existing_baby_styles: list[str],
    add_callback,
    anchor_widget: tk.Widget | None = None,
) -> None:
    normalized_lead_style = normalize_style_identifier(lead_style_code)
    if not normalized_lead_style:
        messagebox.showwarning("Baby style", "Please choose a style first.")
        return

    existing_children = {normalize_style_identifier(item) for item in existing_baby_styles if normalize_style_identifier(item)}
    candidates = [
        str(option.get("style_code", "")).strip()
        for option in self.sample_style_option_rows()
        if str(option.get("style_code", "")).strip()
        and normalize_style_identifier(option.get("style_code")) != normalized_lead_style
        and normalize_style_identifier(option.get("style_code")) not in existing_children
    ]
    candidates = sorted(dict.fromkeys(candidates), key=str.lower)
    if not candidates:
        messagebox.showinfo("Baby style", "No available style can be added as a baby style.")
        return

    dialog = tk.Toplevel(self.root)
    dialog.title(f"Add Baby Style - {normalized_lead_style}")
    dialog.transient(self.root)
    dialog.grab_set()
    dialog.resizable(False, False)

    frame = ttk.Frame(dialog, padding=16)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)
    frame.rowconfigure(2, weight=1)

    ttk.Label(frame, text=f"Search one style to become a baby style of {normalized_lead_style}.").grid(row=0, column=0, sticky="w")
    selected_var = tk.StringVar(value="")
    search_var = tk.StringVar(value="")
    search_entry = ttk.Entry(frame, textvariable=search_var, width=36)
    search_entry.grid(row=1, column=0, sticky="ew", pady=(8, 0))

    result_frame = ttk.Frame(frame)
    result_frame.grid(row=2, column=0, sticky="nsew", pady=(8, 0))
    result_frame.columnconfigure(0, weight=1)
    result_frame.rowconfigure(0, weight=1)

    result_list = tk.Listbox(result_frame, height=8, exportselection=False, font=("Segoe UI", 10))
    result_list.grid(row=0, column=0, sticky="nsew")
    result_scroll = ttk.Scrollbar(result_frame, orient="vertical", command=result_list.yview)
    result_scroll.grid(row=0, column=1, sticky="ns")
    result_list.configure(yscrollcommand=result_scroll.set)

    def get_filtered_candidates() -> list[str]:
        query = search_var.get().strip().lower()
        if not query:
            return list(candidates)
        starts_with = [candidate for candidate in candidates if candidate.lower().startswith(query)]
        contains = [candidate for candidate in candidates if query in candidate.lower() and candidate not in starts_with]
        return starts_with + contains

    def refresh_candidate_list(*_args: object) -> None:
        filtered = get_filtered_candidates()
        result_list.delete(0, tk.END)
        for candidate in filtered:
            result_list.insert(tk.END, candidate)
        if filtered:
            result_list.selection_clear(0, tk.END)
            result_list.selection_set(0)
            result_list.activate(0)
            selected_var.set(filtered[0])
        else:
            selected_var.set("")

    def sync_selected_from_list(_event: object | None = None) -> None:
        selection = result_list.curselection()
        if not selection:
            selected_var.set("")
            return
        selected_var.set(str(result_list.get(selection[0])).strip())

    def save_selection() -> None:
        child_style_code = normalize_style_identifier(selected_var.get())
        if not child_style_code:
            messagebox.showwarning("Baby style", "Please choose one style from the search results.")
            return
        add_callback(child_style_code)
        dialog.destroy()

    search_var.trace_add("write", refresh_candidate_list)
    result_list.bind("<<ListboxSelect>>", sync_selected_from_list)
    result_list.bind("<Double-Button-1>", lambda _event: save_selection())

    button_row = ttk.Frame(frame)
    button_row.grid(row=3, column=0, sticky="e", pady=(14, 0))
    ttk.Button(button_row, text="Cancel", command=dialog.destroy).pack(side="left", padx=(0, 8))
    ttk.Button(button_row, text="Add", command=save_selection).pack(side="left")

    refresh_candidate_list()
    dialog.update_idletasks()
    try:
        if anchor_widget is not None:
            anchor_widget.update_idletasks()
            dialog_width = dialog.winfo_width() or 360
            dialog_height = dialog.winfo_height() or 260
            x = anchor_widget.winfo_rootx() + anchor_widget.winfo_width() + 8
            y = anchor_widget.winfo_rooty() - 4
            screen_width = dialog.winfo_screenwidth()
            screen_height = dialog.winfo_screenheight()
            if x + dialog_width > screen_width - 24:
                x = max(24, anchor_widget.winfo_rootx() - dialog_width - 8)
            if y + dialog_height > screen_height - 48:
                y = max(24, screen_height - dialog_height - 48)
            dialog.geometry(f"+{max(24, x)}+{max(24, y)}")
    except Exception:
        pass
    search_entry.focus_set()


def open_select_sample_bom_item_dialog(
    self,
    style_code: str,
    current_item_id: str = "",
    anchor_widget: tk.Widget | None = None,
) -> dict | None:
    normalized_style = normalize_style_identifier(style_code)
    candidates = [dict(row) for row in getattr(self, "bom_items", []) if isinstance(row, dict)]
    if not candidates and hasattr(self, "refresh_bom_module"):
        try:
            self.refresh_bom_module()
        except Exception:
            pass
        candidates = [dict(row) for row in getattr(self, "bom_items", []) if isinstance(row, dict)]
    if normalized_style:
        style_candidates = [
            row for row in candidates
            if normalize_style_identifier(row.get("style_code")) == normalized_style
        ]
        if style_candidates:
            candidates = style_candidates
    candidates = [
        row for row in candidates
        if str(row.get("item_name", "")).strip() or str(row.get("composition", "")).strip()
    ]
    if not candidates:
        messagebox.showinfo("BOM", "No BOM item is available for this sample yet.")
        return None

    dialog = tk.Toplevel(self.root)
    dialog.title(f"Select BOM Item - {normalized_style or 'All Styles'}")
    dialog.transient(self.root)
    dialog.grab_set()
    dialog.resizable(False, False)

    frame = ttk.Frame(dialog, padding=16)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)
    frame.rowconfigure(2, weight=1)

    ttk.Label(
        frame,
        text=(
            f"Search one BOM item for style {normalized_style}."
            if normalized_style
            else "Search one BOM item to link to this sample."
        ),
    ).grid(row=0, column=0, sticky="w")
    selected_var = tk.StringVar(value="")
    search_var = tk.StringVar(value="")
    search_entry = ttk.Entry(frame, textvariable=search_var, width=48)
    search_entry.grid(row=1, column=0, sticky="ew", pady=(8, 0))

    result_frame = ttk.Frame(frame)
    result_frame.grid(row=2, column=0, sticky="nsew", pady=(8, 0))
    result_frame.columnconfigure(0, weight=1)
    result_frame.rowconfigure(0, weight=1)

    result_list = tk.Listbox(result_frame, height=10, exportselection=False, font=("Segoe UI", 10))
    result_list.grid(row=0, column=0, sticky="nsew")
    result_scroll = ttk.Scrollbar(result_frame, orient="vertical", command=result_list.yview)
    result_scroll.grid(row=0, column=1, sticky="ns")
    result_list.configure(yscrollcommand=result_scroll.set)
    filtered_candidates: list[dict] = []
    result: dict[str, dict | None] = {"item": None}

    def format_candidate(row: dict) -> str:
        row_style = normalize_style_identifier(row.get("style_code")) or "-"
        item_name = str(row.get("item_name", "")).strip() or "(No item name)"
        composition = str(row.get("composition", "")).strip()
        return f"{row_style} | {item_name}" if not composition else f"{row_style} | {item_name} | {composition}"

    def get_filtered_candidates() -> list[dict]:
        query = search_var.get().strip().lower()
        if not query:
            return list(candidates)

        def score(row: dict) -> tuple[int, str]:
            row_style = normalize_style_identifier(row.get("style_code"))
            item_name = str(row.get("item_name", "")).strip()
            composition = str(row.get("composition", "")).strip()
            item_code = str(row.get("item_code", "")).strip()
            combined = " ".join(part for part in (row_style, item_name, composition, item_code) if part).lower()
            if item_name.lower().startswith(query):
                return (0, combined)
            if row_style.lower().startswith(query):
                return (1, combined)
            return (2, combined)

        matches = [
            row for row in candidates
            if query in " ".join(
                part for part in (
                    normalize_style_identifier(row.get("style_code")),
                    str(row.get("item_name", "")).strip(),
                    str(row.get("composition", "")).strip(),
                    str(row.get("item_code", "")).strip(),
                )
                if part
            ).lower()
        ]
        matches.sort(key=score)
        return matches

    def refresh_candidate_list(*_args: object) -> None:
        nonlocal filtered_candidates
        filtered_candidates = get_filtered_candidates()
        result_list.delete(0, tk.END)
        selected_index = 0
        for index, candidate in enumerate(filtered_candidates):
            result_list.insert(tk.END, format_candidate(candidate))
            if current_item_id and str(candidate.get("item_id", "")).strip() == current_item_id:
                selected_index = index
        if filtered_candidates:
            result_list.selection_clear(0, tk.END)
            result_list.selection_set(selected_index)
            result_list.activate(selected_index)
            selected_var.set(str(filtered_candidates[selected_index].get("item_id", "")).strip())
        else:
            selected_var.set("")

    def sync_selected_from_list(_event: object | None = None) -> None:
        selection = result_list.curselection()
        if not selection:
            selected_var.set("")
            return
        selected_row = filtered_candidates[selection[0]] if selection[0] < len(filtered_candidates) else {}
        selected_var.set(str(selected_row.get("item_id", "")).strip())

    def save_selection() -> None:
        selected_item_id = selected_var.get().strip()
        if not selected_item_id:
            messagebox.showwarning("BOM", "Please choose one BOM item from the search results.")
            return
        chosen = next(
            (row for row in filtered_candidates if str(row.get("item_id", "")).strip() == selected_item_id),
            None,
        )
        if chosen is None:
            chosen = next(
                (row for row in candidates if str(row.get("item_id", "")).strip() == selected_item_id),
                None,
            )
        result["item"] = dict(chosen) if isinstance(chosen, dict) else None
        dialog.destroy()

    search_var.trace_add("write", refresh_candidate_list)
    result_list.bind("<<ListboxSelect>>", sync_selected_from_list)
    result_list.bind("<Double-Button-1>", lambda _event: save_selection())

    button_row = ttk.Frame(frame)
    button_row.grid(row=3, column=0, sticky="e", pady=(14, 0))
    ttk.Button(button_row, text="Cancel", command=dialog.destroy).pack(side="left", padx=(0, 8))
    ttk.Button(button_row, text="Use BOM Item", command=save_selection).pack(side="left")

    refresh_candidate_list()
    dialog.update_idletasks()
    try:
        if anchor_widget is not None:
            anchor_widget.update_idletasks()
            dialog_width = dialog.winfo_width() or 520
            dialog_height = dialog.winfo_height() or 320
            x = anchor_widget.winfo_rootx() + anchor_widget.winfo_width() + 8
            y = anchor_widget.winfo_rooty() - 4
            screen_width = dialog.winfo_screenwidth()
            screen_height = dialog.winfo_screenheight()
            if x + dialog_width > screen_width - 24:
                x = max(24, anchor_widget.winfo_rootx() - dialog_width - 8)
            if y + dialog_height > screen_height - 48:
                y = max(24, screen_height - dialog_height - 48)
            dialog.geometry(f"+{max(24, x)}+{max(24, y)}")
    except Exception:
        pass
    search_entry.focus_set()
    dialog.wait_window()
    return dict(result["item"]) if isinstance(result.get("item"), dict) else None


def open_new_sample_dialog(self, source_row: dict | None = None, *, duplicate: bool = False) -> None:
    style_options = self.sample_style_option_rows()
    initial_row = dict(source_row) if isinstance(source_row, dict) else None
    is_edit_mode = initial_row is not None and not duplicate
    required_permission = "action.samples.copy" if duplicate else ("action.samples.edit" if is_edit_mode else "action.samples.new")
    if not self.is_action_allowed(required_permission):
        return
    dialog_title = "Edit Sample" if is_edit_mode else ("Copy Sample" if duplicate else "New Sample")
    dialog_heading = dialog_title
    dialog = tk.Toplevel(self.root)
    dialog.title(dialog_title)
    dialog.minsize(900, 680)
    dialog.resizable(True, True)
    dialog.transient(self.root)
    dialog.grab_set()
    _apply_window_icon(dialog)

    default_request_number = str(initial_row.get("request_number", "")).strip() if is_edit_mode and initial_row else self.make_sample_request_number()
    request_number_var = tk.StringVar(value=default_request_number)
    style_var = tk.StringVar()
    image_var = tk.StringVar()
    image_display_var = tk.StringVar()
    description_var = tk.StringVar()
    mr_pic_var = tk.StringVar(value=str(initial_row.get("mr_pic", "")).strip() if initial_row else "")
    pattern_pic_var = tk.StringVar(value=str(initial_row.get("pattern_pic", "")).strip() if initial_row else "")
    sewer_pic_var = tk.StringVar(value=str(initial_row.get("sewer_pic", "")).strip() if initial_row else "")
    ie_pic_var = tk.StringVar(value=str(initial_row.get("ie_pic", "")).strip() if initial_row else "")
    material_date_var = tk.StringVar(value=str(initial_row.get("material_date", "")).strip() if initial_row else "")
    deadline_var = tk.StringVar(value=str(initial_row.get("deadline", "")).strip() if initial_row else "")
    sample_type_var = tk.StringVar(value=str(initial_row.get("sample_type", "")).strip() if initial_row else "")
    cutting_date_var = tk.StringVar(value=str(initial_row.get("cutting_date", "")).strip() if initial_row else "")
    sewing_date_var = tk.StringVar(value=str(initial_row.get("sewing_date", "")).strip() if initial_row else "")
    finishing_date_var = tk.StringVar(value=str(initial_row.get("finishing_date", "")).strip() if initial_row else "")
    sending_date_var = tk.StringVar(value=str(initial_row.get("sending_date", "")).strip() if initial_row else "")
    fit_status_var = tk.StringVar(value=str(initial_row.get("fit_evals_status", "")).strip() if initial_row else "")
    upload_comment_date_var = tk.StringVar(value=str(initial_row.get("upload_comment_date", "")).strip() if initial_row else "")
    buyer_comment_date_var = tk.StringVar(value=str(initial_row.get("buyer_comment_date", "")).strip() if initial_row else "")
    bom_item_name_var = tk.StringVar(value=str(initial_row.get("bom_item_name", "")).strip() if initial_row else "")
    bom_composition_var = tk.StringVar(value=str(initial_row.get("bom_composition", "")).strip() if initial_row else "")
    status_var = tk.StringVar(value="")
    selected_style: dict[str, str] = {}
    size_options = self.sample_size_options()
    sample_type_options = self.sample_type_options_list()
    role_options = self.sample_pic_options_by_role()
    mr_pic_options = role_options["mr_pic"]
    pattern_pic_options = role_options["pattern_pic"]
    sewer_pic_options = role_options["sewer_pic"]
    style_value_map = {option["label"].lower(): option for option in style_options}
    color_options_cache = ["Available"]
    color_size_rows: list[dict[str, object]] = []
    baby_style_panel_state: dict[str, object] = {}
    bom_panel_state: dict[str, object] = {}
    sample_baby_styles: list[str] = parse_sample_baby_styles(initial_row.get("baby_styles", "")) if initial_row else []
    selected_bom_item: dict[str, str] = {
        "item_id": str(initial_row.get("bom_item_id", "")).strip() if initial_row else "",
        "style_code": normalize_style_identifier(initial_row.get("style_code") or initial_row.get("style")) if initial_row else "",
        "item_name": str(initial_row.get("bom_item_name", "")).strip() if initial_row else "",
        "composition": str(initial_row.get("bom_composition", "")).strip() if initial_row else "",
    }

    frame = ttk.Frame(dialog, padding=18)
    frame.pack(fill="both", expand=True)
    frame.columnconfigure(0, weight=1)
    frame.rowconfigure(1, weight=1)

    ttk.Label(frame, text=dialog_heading, style="Section.TLabel").grid(row=0, column=0, sticky="w")

    notebook = ttk.Notebook(frame)
    notebook.grid(row=1, column=0, sticky="nsew", pady=(12, 0))

    basic_tab = ttk.Frame(notebook, padding=14)
    materials_tab = ttk.Frame(notebook, padding=14)
    processes_tab = ttk.Frame(notebook, padding=14)
    comment_tab = ttk.Frame(notebook, padding=14)
    notebook.add(basic_tab, text="Basic Information")
    notebook.add(materials_tab, text="Materials")
    notebook.add(processes_tab, text="Processes")
    notebook.add(comment_tab, text="Comment")

    for tab in (basic_tab, materials_tab, processes_tab, comment_tab):
        tab.columnconfigure(1, weight=1)
    basic_tab.columnconfigure(2, weight=0)
    basic_tab.columnconfigure(3, weight=0)
    basic_tab.columnconfigure(4, weight=0)

    preview_holder = ttk.Label(basic_tab, text="No image", relief="solid", anchor="center")
    preview_holder.grid(row=0, column=2, rowspan=8, sticky="n", padx=(20, 0))
    preview_holder.image = None  # type: ignore[attr-defined]

    style_search_frame = ttk.Frame(basic_tab)
    style_search_frame.grid(row=1, column=1, sticky="ew", pady=6)
    style_search_frame.columnconfigure(0, weight=1)

    style_search_entry = ttk.Entry(style_search_frame, textvariable=style_var)
    style_search_entry.grid(row=0, column=0, sticky="ew")

    suggestion_frame = ttk.Frame(style_search_frame, borderwidth=1, relief="solid")
    suggestion_listbox = tk.Listbox(suggestion_frame, height=6, exportselection=False)
    suggestion_scrollbar = ttk.Scrollbar(suggestion_frame, orient="vertical", command=suggestion_listbox.yview)
    suggestion_listbox.configure(yscrollcommand=suggestion_scrollbar.set)
    suggestion_listbox.pack(side="left", fill="both", expand=True)
    suggestion_scrollbar.pack(side="right", fill="y")

    def format_sample_quantity(quantity_text: str) -> str:
        cleaned = quantity_text.strip()
        if not cleaned:
            return ""
        normalized = cleaned.lower().replace("pieces", "pcs").replace("piece", "pcs")
        return cleaned if "pcs" in normalized else f"{cleaned} pcs"

    def score_style_option(option: dict[str, str], query_text: str) -> tuple[int, str]:
        label = option.get("label", "")
        style_code = option.get("style_code", "")
        style_name = option.get("style_name", "")
        query = query_text.lower()
        if not query:
            return (3, label.lower())
        if style_code.lower().startswith(query):
            return (0, label.lower())
        if style_name.lower().startswith(query):
            return (1, label.lower())
        if query in style_code.lower():
            return (2, label.lower())
        return (3, label.lower())

    def find_style_option(query_text: str) -> dict[str, str] | None:
        query = query_text.strip().lower()
        if not query:
            return None
        exact = style_value_map.get(query)
        if exact is not None:
            return exact
        matches = [
            option
            for option in style_options
            if query in option.get("style_code", "").lower()
            or query in option.get("style_name", "").lower()
            or query in option.get("label", "").lower()
        ]
        if not matches:
            return None
        matches.sort(key=lambda option: score_style_option(option, query))
        return matches[0]

    def hide_style_suggestions() -> None:
        if suggestion_frame.winfo_manager():
            suggestion_frame.grid_remove()

    def show_style_suggestions(matches: list[dict[str, str]]) -> None:
        suggestion_listbox.delete(0, tk.END)
        for option in matches:
            suggestion_listbox.insert(tk.END, option.get("label", ""))
        if matches:
            suggestion_listbox.selection_clear(0, tk.END)
            suggestion_listbox.selection_set(0)
            suggestion_listbox.activate(0)
            if not suggestion_frame.winfo_manager():
                suggestion_frame.grid(row=1, column=0, sticky="ew", pady=(2, 0))
        else:
            hide_style_suggestions()

    def update_color_row_values() -> None:
        for row in color_size_rows:
            color_combo = row["color_combo"]
            color_var = row["color_var"]
            color_combo.configure(values=color_options_cache)
            current_value = str(color_var.get() or "").strip()
            if current_value not in color_options_cache:
                color_var.set(color_options_cache[0] if color_options_cache else "")

    def refresh_color_size_rows() -> None:
        for child in color_size_frame.winfo_children():
            child.destroy()

        ttk.Label(color_size_frame, text="Color").grid(row=0, column=0, sticky="w", padx=(0, 8), pady=(0, 6))
        ttk.Label(color_size_frame, text="Size").grid(row=0, column=1, sticky="w", padx=(0, 8), pady=(0, 6))
        ttk.Label(color_size_frame, text="Quantity").grid(row=0, column=2, sticky="w", padx=(0, 8), pady=(0, 6))

        for row_index, row in enumerate(color_size_rows, start=1):
            color_combo = ttk.Combobox(
                color_size_frame,
                textvariable=row["color_var"],
                state="readonly",
                values=color_options_cache,
                width=22,
            )
            color_combo.grid(row=row_index, column=0, sticky="ew", padx=(0, 8), pady=4)
            size_combo = ttk.Combobox(
                color_size_frame,
                textvariable=row["size_var"],
                state="readonly",
                values=size_options,
                width=12,
            )
            size_combo.grid(row=row_index, column=1, sticky="ew", padx=(0, 8), pady=4)
            quantity_entry = ttk.Entry(color_size_frame, textvariable=row["quantity_var"], width=14)
            quantity_entry.grid(row=row_index, column=2, sticky="ew", padx=(0, 8), pady=4)

            ttk.Button(
                color_size_frame,
                text="+",
                width=3,
                command=add_color_size_row,
            ).grid(row=row_index, column=3, sticky="w", padx=(0, 6), pady=4)

            remove_button_state = "normal" if len(color_size_rows) > 1 else "disabled"
            ttk.Button(
                color_size_frame,
                text="-",
                width=3,
                command=lambda row_ref=row: remove_color_size_row(row_ref),
                state=remove_button_state,
            ).grid(row=row_index, column=4, sticky="w", pady=4)

            row["color_combo"] = color_combo
            row["size_combo"] = size_combo
            row["quantity_entry"] = quantity_entry

    def resolve_selected_style_code() -> str:
        if selected_style.get("style_code"):
            return normalize_style_identifier(selected_style.get("style_code"))
        matched_style = find_style_option(style_var.get())
        if matched_style is not None:
            return normalize_style_identifier(matched_style.get("style_code"))
        return normalize_style_identifier(style_var.get())

    def add_sample_baby_style(style_code: str) -> None:
        normalized_style = normalize_style_identifier(style_code)
        current_style_code = resolve_selected_style_code()
        if not normalized_style or normalized_style == current_style_code:
            return
        if normalized_style not in sample_baby_styles:
            sample_baby_styles.append(normalized_style)
        refresh_baby_style_panel()

    def remove_sample_baby_style(style_code: str) -> None:
        normalized_style = normalize_style_identifier(style_code)
        if not normalized_style:
            return
        sample_baby_styles[:] = [item for item in sample_baby_styles if normalize_style_identifier(item) != normalized_style]
        refresh_baby_style_panel()

    def refresh_baby_style_panel() -> None:
        host = baby_style_panel_state.get("host")
        if host is None:
            return
        for child in host.winfo_children():
            child.destroy()

        current_style_code = resolve_selected_style_code()
        if not current_style_code:
            ttk.Label(host, text="Choose a style first.", foreground="#6c748c").grid(row=0, column=0, sticky="w")
            return
        sample_baby_styles[:] = [item for item in sample_baby_styles if normalize_style_identifier(item) != current_style_code]

        add_button = ttk.Button(
            host,
            text="+",
            width=3,
        )
        add_button.configure(
            command=lambda button_ref=add_button: self.open_add_sample_baby_style_dialog(
                current_style_code,
                list(sample_baby_styles),
                add_sample_baby_style,
                button_ref,
            )
        )
        add_button.grid(row=0, column=0, sticky="w", pady=(0, 6))
        baby_style_panel_state["add_button"] = add_button

        ttk.Label(host, text="Add baby style").grid(row=0, column=1, sticky="w", padx=(6, 0), pady=(0, 6))

        if sample_baby_styles:
            for row_index, child_style_code in enumerate(sample_baby_styles, start=1):
                ttk.Label(host, text=child_style_code).grid(row=row_index, column=0, sticky="w", pady=(0, 4))
                ttk.Button(
                    host,
                    text="-",
                    width=3,
                    command=lambda style_ref=child_style_code: remove_sample_baby_style(style_ref),
                ).grid(row=row_index, column=1, sticky="w", padx=(6, 0), pady=(0, 4))
        else:
            ttk.Label(host, text="No baby style added yet.", foreground="#6c748c").grid(row=1, column=0, columnspan=2, sticky="w")

    def clear_selected_bom_item() -> None:
        selected_bom_item["item_id"] = ""
        selected_bom_item["style_code"] = ""
        selected_bom_item["item_name"] = ""
        selected_bom_item["composition"] = ""
        bom_item_name_var.set("")
        bom_composition_var.set("")
        refresh_bom_panel()

    def apply_selected_bom_item(row: dict | None) -> None:
        if not isinstance(row, dict):
            return
        selected_bom_item["item_id"] = str(row.get("item_id", "")).strip()
        selected_bom_item["style_code"] = normalize_style_identifier(row.get("style_code"))
        selected_bom_item["item_name"] = str(row.get("item_name", "")).strip()
        selected_bom_item["composition"] = str(row.get("composition", "")).strip()
        bom_item_name_var.set(selected_bom_item["item_name"])
        bom_composition_var.set(selected_bom_item["composition"])
        refresh_bom_panel()

    def refresh_bom_panel() -> None:
        host = bom_panel_state.get("host")
        if host is None:
            return
        for child in host.winfo_children():
            child.destroy()
        host.columnconfigure(1, weight=1)
        current_style_code = resolve_selected_style_code()

        add_button = ttk.Button(host, text="+", width=3)
        add_button.configure(
            command=lambda button_ref=add_button: apply_selected_bom_item(
                self.open_select_sample_bom_item_dialog(
                    current_style_code,
                    selected_bom_item.get("item_id", ""),
                    button_ref,
                )
            )
        )
        add_button.grid(row=0, column=0, sticky="nw", pady=(0, 6))
        bom_panel_state["add_button"] = add_button

        if selected_bom_item.get("item_name"):
            info_frame = ttk.Frame(host)
            info_frame.grid(row=0, column=1, sticky="ew", padx=(6, 0))
            info_frame.columnconfigure(0, weight=1)
            ttk.Label(info_frame, text=selected_bom_item.get("item_name", "")).grid(row=0, column=0, sticky="w")
            composition_text = selected_bom_item.get("composition", "")
            ttk.Label(
                info_frame,
                text=composition_text or "No composition",
                foreground="#6c748c" if not composition_text else "",
                wraplength=420,
                justify="left",
            ).grid(row=1, column=0, sticky="w", pady=(2, 0))
            ttk.Button(
                info_frame,
                text="-",
                width=3,
                command=clear_selected_bom_item,
            ).grid(row=0, column=1, rowspan=2, sticky="ne", padx=(8, 0))
        else:
            empty_text = "No BOM item selected yet." if current_style_code else "Choose a style first, then add one BOM item."
            ttk.Label(host, text=empty_text, foreground="#6c748c").grid(row=0, column=1, sticky="w", padx=(6, 0))

    def add_color_size_row(defaults: dict[str, str] | None = None) -> None:
        row_defaults = defaults or {}
        color_var = tk.StringVar(value=row_defaults.get("color", color_options_cache[0] if color_options_cache else ""))
        size_var = tk.StringVar(value=row_defaults.get("size", ""))
        quantity_var = tk.StringVar(value=row_defaults.get("quantity", ""))
        color_size_rows.append(
            {
                "color_var": color_var,
                "size_var": size_var,
                "quantity_var": quantity_var,
            }
        )
        refresh_color_size_rows()

    def remove_color_size_row(row_ref: dict[str, object]) -> None:
        if len(color_size_rows) <= 1:
            return
        color_size_rows.remove(row_ref)
        refresh_color_size_rows()

    def apply_style_selection(style_option: dict[str, str] | None) -> None:
        previous_style_code = resolve_selected_style_code()
        selected_style.clear()
        if style_option:
            selected_style.update(style_option)
            style_var.set(style_option.get("label", ""))
            resolved_image = str(style_option.get("image", "")).strip()
            image_var.set(resolved_image)
            image_display_var.set(format_sample_image_display(resolved_image))
            description_var.set(style_option.get("description", ""))
            color_options_cache[:] = self.extract_tech_pack_colors(style_option.get("style_code", ""))
            update_color_row_values()
            refresh_baby_style_panel()
            preview_photo = self.create_sample_dialog_preview_photo(resolved_image)
            if preview_photo is not None:
                preview_holder.configure(image=preview_photo, text="")
                preview_holder.image = preview_photo  # type: ignore[attr-defined]
            else:
                preview_holder.configure(image="", text="No image")
                preview_holder.image = None  # type: ignore[attr-defined]
        else:
            image_var.set("")
            image_display_var.set("")
            description_var.set("")
            color_options_cache[:] = ["Available"]
            update_color_row_values()
            refresh_baby_style_panel()
            preview_holder.configure(image="", text="No image")
            preview_holder.image = None  # type: ignore[attr-defined]
        current_style_code = resolve_selected_style_code()
        selected_bom_style = normalize_style_identifier(selected_bom_item.get("style_code"))
        if selected_bom_style and current_style_code and selected_bom_style != current_style_code:
            clear_selected_bom_item()
        elif selected_bom_style and not current_style_code:
            clear_selected_bom_item()
        elif previous_style_code != current_style_code:
            refresh_bom_panel()

    def update_style_suggestions(_event: object | None = None) -> None:
        query = style_var.get().strip()
        if not query:
            show_style_suggestions(style_options[:12])
            return
        matches = [
            option
            for option in style_options
            if query.lower() in option.get("style_code", "").lower()
            or query.lower() in option.get("style_name", "").lower()
            or query.lower() in option.get("label", "").lower()
        ]
        matches.sort(key=lambda option: score_style_option(option, query))
        show_style_suggestions(matches[:12])

    def choose_style_from_suggestion(_event: object | None = None) -> str:
        selection = suggestion_listbox.curselection()
        if not selection:
            return "break"
        selected_label = str(suggestion_listbox.get(selection[0]))
        selected = style_value_map.get(selected_label.lower())
        if selected is not None:
            apply_style_selection(selected)
        hide_style_suggestions()
        style_search_entry.focus_set()
        return "break"

    def on_style_entry_focus_in(_event: object | None = None) -> None:
        update_style_suggestions()

    def on_style_entry_focus_out(_event: object | None = None) -> None:
        self.schedule_root_after(120, hide_style_suggestions)
        selected = find_style_option(style_var.get())
        if selected is not None:
            apply_style_selection(selected)

    ttk.Label(basic_tab, text="Request #").grid(row=0, column=0, sticky="w", padx=(0, 10), pady=6)
    ttk.Entry(basic_tab, textvariable=request_number_var).grid(row=0, column=1, sticky="ew", pady=6)
    ttk.Label(basic_tab, text="Style *").grid(row=1, column=0, sticky="w", padx=(0, 10), pady=6)
    ttk.Label(basic_tab, text="Image").grid(row=2, column=0, sticky="w", padx=(0, 10), pady=6)
    ttk.Entry(basic_tab, textvariable=image_display_var, state="readonly").grid(row=2, column=1, sticky="ew", pady=6)
    ttk.Label(basic_tab, text="Description").grid(row=3, column=0, sticky="nw", padx=(0, 10), pady=6)
    description_text = tk.Text(basic_tab, height=5, wrap="word")
    description_text.grid(row=3, column=1, sticky="ew", pady=6)
    ttk.Label(basic_tab, text="MR PIC").grid(row=4, column=0, sticky="w", padx=(0, 10), pady=6)
    mr_pic_combo = ttk.Combobox(basic_tab, textvariable=mr_pic_var, state="readonly", values=mr_pic_options)
    mr_pic_combo.grid(row=4, column=1, sticky="ew", pady=6)
    ttk.Label(basic_tab, text="Sample Type").grid(row=5, column=0, sticky="w", padx=(0, 10), pady=6)
    sample_type_combo = ttk.Combobox(
        basic_tab,
        textvariable=sample_type_var,
        state="readonly",
        values=sample_type_options,
    )
    sample_type_combo.grid(row=5, column=1, sticky="ew", pady=6)
    ttk.Label(basic_tab, text="Color / Size / Quantity").grid(row=6, column=0, sticky="nw", padx=(0, 10), pady=6)
    color_size_frame = ttk.Frame(basic_tab)
    color_size_frame.grid(row=6, column=1, sticky="ew", pady=6)
    color_size_frame.columnconfigure(0, weight=1)
    color_size_frame.columnconfigure(1, weight=0)
    color_size_frame.columnconfigure(2, weight=0)
    ttk.Label(basic_tab, text="Baby style").grid(row=7, column=0, sticky="nw", padx=(0, 10), pady=6)
    baby_style_frame = ttk.Frame(basic_tab)
    baby_style_frame.grid(row=7, column=1, sticky="ew", pady=6)
    baby_style_frame.columnconfigure(1, weight=1)
    baby_style_panel_state["host"] = baby_style_frame
    ttk.Label(basic_tab, text="Deadline").grid(row=8, column=0, sticky="w", padx=(0, 10), pady=6)
    deadline_entry = ttk.Entry(basic_tab, textvariable=deadline_var)
    deadline_entry.grid(row=8, column=1, sticky="ew", pady=6)
    self.bind_date_entry_picker(deadline_entry, deadline_var)

    ttk.Label(materials_tab, text="Material Date").grid(row=0, column=0, sticky="w", padx=(0, 10), pady=6)
    material_entry = ttk.Entry(materials_tab, textvariable=material_date_var)
    material_entry.grid(row=0, column=1, sticky="ew", pady=6)
    self.bind_date_entry_picker(material_entry, material_date_var)
    ttk.Label(materials_tab, text="BOM").grid(row=1, column=0, sticky="nw", padx=(0, 10), pady=6)
    sample_bom_frame = ttk.Frame(materials_tab)
    sample_bom_frame.grid(row=1, column=1, sticky="ew", pady=6)
    sample_bom_frame.columnconfigure(1, weight=1)
    bom_panel_state["host"] = sample_bom_frame

    ttk.Label(processes_tab, text="Pattern P.I.C").grid(row=0, column=0, sticky="w", padx=(0, 10), pady=6)
    ttk.Combobox(processes_tab, textvariable=pattern_pic_var, state="readonly", values=pattern_pic_options).grid(row=0, column=1, sticky="ew", pady=6)
    ttk.Label(processes_tab, text="Sewer P.I.C").grid(row=1, column=0, sticky="w", padx=(0, 10), pady=6)
    ttk.Combobox(processes_tab, textvariable=sewer_pic_var, state="readonly", values=sewer_pic_options).grid(row=1, column=1, sticky="ew", pady=6)
    process_fields = [
        ("IE P.I.C", ie_pic_var),
        ("Cutting date", cutting_date_var),
        ("Sewing date", sewing_date_var),
        ("Finishing date", finishing_date_var),
        ("Sending Date", sending_date_var),
    ]
    for row_index, (label_text, variable) in enumerate(process_fields, start=2):
        ttk.Label(processes_tab, text=label_text).grid(row=row_index, column=0, sticky="w", padx=(0, 10), pady=6)
        entry = ttk.Entry(processes_tab, textvariable=variable)
        entry.grid(row=row_index, column=1, sticky="ew", pady=6)
        if "date" in label_text.lower():
            self.bind_date_entry_picker(entry, variable)

    ttk.Label(comment_tab, text="Upload comment date").grid(row=0, column=0, sticky="w", padx=(0, 10), pady=6)
    upload_entry = ttk.Entry(comment_tab, textvariable=upload_comment_date_var)
    upload_entry.grid(row=0, column=1, sticky="ew", pady=6)
    self.bind_date_entry_picker(upload_entry, upload_comment_date_var)
    ttk.Label(comment_tab, text="Buyer comment date").grid(row=1, column=0, sticky="w", padx=(0, 10), pady=6)
    buyer_comment_entry = ttk.Entry(comment_tab, textvariable=buyer_comment_date_var)
    buyer_comment_entry.grid(row=1, column=1, sticky="ew", pady=6)
    self.bind_date_entry_picker(buyer_comment_entry, buyer_comment_date_var)
    ttk.Label(comment_tab, text="Fit evals status").grid(row=2, column=0, sticky="w", padx=(0, 10), pady=6)
    ttk.Entry(comment_tab, textvariable=fit_status_var).grid(row=2, column=1, sticky="ew", pady=6)
    ttk.Label(comment_tab, text="Remark").grid(row=3, column=0, sticky="nw", padx=(0, 10), pady=6)
    remark_text = tk.Text(comment_tab, height=8, wrap="word")
    remark_text.grid(row=3, column=1, sticky="ew", pady=6)

    status_label = ttk.Label(frame, textvariable=status_var, foreground="#8a4b1f")
    status_label.grid(row=2, column=0, sticky="w", pady=(12, 0))

    actions = ttk.Frame(frame)
    actions.grid(row=3, column=0, sticky="e", pady=(12, 0))

    def submit() -> None:
        if not self.is_action_allowed(required_permission):
            status_var.set("You do not have permission to save this sample.")
            return
        chosen_style = dict(selected_style)
        if not chosen_style:
            matched_style = find_style_option(style_var.get())
            if matched_style is not None:
                chosen_style = dict(matched_style)
        if not chosen_style:
            status_var.set("Please choose a style from Tech Pack.")
            notebook.select(basic_tab)
            return

        color_values: list[str] = []
        size_values: list[str] = []
        quantity_values: list[str] = []
        for row in color_size_rows:
            color_value = str(row["color_var"].get() or "").strip()
            size_value = str(row["size_var"].get() or "").strip()
            quantity_value = str(row["quantity_var"].get() or "").strip()
            has_any_value = bool(color_value or size_value or quantity_value)
            if not has_any_value:
                continue
            if not color_value or not size_value or not quantity_value:
                status_var.set("Please complete Color, Size, and Quantity on every sample row.")
                notebook.select(basic_tab)
                return
            color_values.append(color_value)
            size_values.append(size_value)
            quantity_values.append(format_sample_quantity(quantity_value))

        description_text_value = description_text.get("1.0", tk.END).strip()
        remark_value = remark_text.get("1.0", tk.END).strip()
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        request_number = request_number_var.get().strip() or self.make_sample_request_number()
        new_row = self.default_sample_record()
        new_row.update(
            {
                "record_id": str(initial_row.get("record_id", "")).strip() if is_edit_mode and initial_row else str(uuid.uuid4()),
                "created_at": str(initial_row.get("created_at", "")).strip() if is_edit_mode and initial_row else timestamp,
                "updated_at": timestamp,
                "request_number": request_number,
                "style": chosen_style.get("style_code", ""),
                "style_code": chosen_style.get("style_code", ""),
                "style_name": chosen_style.get("style_name", ""),
                "baby_styles": format_sample_baby_styles(sample_baby_styles),
                "image": chosen_style.get("image", ""),
                "description": description_text_value,
                "mr_pic": mr_pic_var.get().strip(),
                "pattern_pic": pattern_pic_var.get().strip(),
                "sewer_pic": sewer_pic_var.get().strip(),
                "ie_pic": ie_pic_var.get().strip(),
                "material_date": material_date_var.get().strip(),
                "bom_item_id": selected_bom_item.get("item_id", ""),
                "bom_item_name": bom_item_name_var.get().strip(),
                "bom_composition": bom_composition_var.get().strip(),
                "deadline": deadline_var.get().strip(),
                "sample_type": sample_type_var.get().strip(),
                "color": "\n".join(color_values),
                "size": "\n".join(size_values),
                "quantity": "\n".join(quantity_values),
                "cutting_date": cutting_date_var.get().strip(),
                "sewing_date": sewing_date_var.get().strip(),
                "finishing_date": finishing_date_var.get().strip(),
                "sending_date": sending_date_var.get().strip(),
                "fit_evals_status": fit_status_var.get().strip(),
                "upload_comment_date": upload_comment_date_var.get().strip(),
                "buyer_comment_date": buyer_comment_date_var.get().strip(),
                "remark": remark_value,
            }
        )
        if hasattr(self, "append_sample_save_debug_log") and hasattr(self, "build_sample_save_debug_snapshot"):
            try:
                self.append_sample_save_debug_log(
                    "open_new_sample_dialog:submit_before_save "
                    + json.dumps(
                        {
                            "mode": "edit" if is_edit_mode else ("copy" if duplicate else "new"),
                            "snapshot": self.build_sample_save_debug_snapshot(new_row),
                        },
                        ensure_ascii=False,
                    )
                )
            except Exception:
                pass
        saved_row = self.save_sample_record_to_source(new_row)
        if hasattr(self, "append_sample_save_debug_log") and hasattr(self, "build_sample_save_debug_snapshot"):
            try:
                self.append_sample_save_debug_log(
                    "open_new_sample_dialog:submit_after_save "
                    + json.dumps(
                        {
                            "mode": "edit" if is_edit_mode else ("copy" if duplicate else "new"),
                            "snapshot": self.build_sample_save_debug_snapshot(saved_row),
                        },
                        ensure_ascii=False,
                    )
                )
            except Exception:
                pass
        target_id = str(saved_row.get("record_id", "")).strip()
        rows: list[dict] = []
        replaced = False
        for existing_row in self.sample_rows:
            existing_id = str(existing_row.get("record_id", "")).strip()
            if target_id and existing_id == target_id:
                rows.append(saved_row)
                replaced = True
            else:
                rows.append(existing_row)
        if not replaced:
            rows.insert(0, saved_row)
        normalized_rows = self.normalize_sample_records(rows)
        self.sample_rows = self.enrich_sample_rows(normalized_rows)
        self.apply_samples_filter()
        self.rebuild_tech_pack_rows_from_current_state()
        if is_edit_mode:
            self.set_status(f"Updated sample {request_number}.")
        elif duplicate:
            self.set_status(f"Copied sample {request_number}.")
        else:
            self.set_status(f"Created sample {request_number}.")
        dialog.destroy()

    ttk.Button(actions, text="Cancel", command=dialog.destroy).pack(side="left", padx=(0, 8))
    save_button = ttk.Button(actions, text="Save", command=submit)
    save_button.pack(side="left")
    self.register_permission_widget(required_permission, save_button)
    save_button.configure(state="normal" if self.is_action_allowed(required_permission) else "disabled")

    def sync_description_from_var(*_args: object) -> None:
        description_text.delete("1.0", tk.END)
        description_text.insert("1.0", description_var.get())

    style_search_entry.bind("<KeyRelease>", update_style_suggestions)
    style_search_entry.bind("<FocusIn>", on_style_entry_focus_in)
    style_search_entry.bind("<FocusOut>", on_style_entry_focus_out)
    style_search_entry.bind("<Down>", lambda _event: (suggestion_listbox.focus_set(), "break")[1] if suggestion_listbox.size() else "break")
    suggestion_listbox.bind("<Double-Button-1>", choose_style_from_suggestion)
    suggestion_listbox.bind("<Return>", choose_style_from_suggestion)
    suggestion_listbox.bind("<Escape>", lambda _event: hide_style_suggestions() or "break")

    description_var.trace_add("write", sync_description_from_var)
    if initial_row:
        initial_style_code = str(initial_row.get("style_code") or initial_row.get("style") or "").strip()
        matched_initial_style = next(
            (option for option in style_options if normalize_style_identifier(option.get("style_code")) == normalize_style_identifier(initial_style_code)),
            None,
        )
        if matched_initial_style is not None:
            apply_style_selection(matched_initial_style)
        else:
            style_var.set(initial_style_code)
            resolved_initial_image = self.resolve_sample_row_image(initial_row)
            image_var.set(resolved_initial_image)
            image_display_var.set(format_sample_image_display(resolved_initial_image))
            description_var.set(str(initial_row.get("description", "")).strip())
            preview_photo = self.create_sample_dialog_preview_photo(resolved_initial_image)
            if preview_photo is not None:
                preview_holder.configure(image=preview_photo, text="")
                preview_holder.image = preview_photo  # type: ignore[attr-defined]
            else:
                preview_holder.configure(image="", text="No image")
                preview_holder.image = None  # type: ignore[attr-defined]
            color_options_cache[:] = self.extract_tech_pack_colors(initial_style_code)
            update_color_row_values()
            refresh_baby_style_panel()
    else:
        apply_style_selection(None)

    initial_colors = [line.strip() for line in str(initial_row.get("color", "")).splitlines() if line.strip()] if initial_row else []
    initial_sizes = [line.strip() for line in str(initial_row.get("size", "")).splitlines() if line.strip()] if initial_row else []
    initial_quantities = [line.strip() for line in str(initial_row.get("quantity", "")).splitlines() if line.strip()] if initial_row else []
    row_count = max(len(initial_colors), len(initial_sizes), len(initial_quantities), 1)
    for index in range(row_count):
        add_color_size_row(
            {
                "color": initial_colors[index] if index < len(initial_colors) else (color_options_cache[0] if color_options_cache else ""),
                "size": initial_sizes[index] if index < len(initial_sizes) else "",
                "quantity": initial_quantities[index] if index < len(initial_quantities) else "",
            }
        )
    if initial_row:
        description_text.delete("1.0", tk.END)
        description_text.insert("1.0", str(initial_row.get("description", "")).strip())
        remark_text.insert("1.0", str(initial_row.get("remark", "")).strip())
        if duplicate:
            request_number_var.set(self.make_sample_request_number())
    else:
        add_color_size_row()
    refresh_baby_style_panel()
    refresh_bom_panel()
    self.center_dialog(dialog, min_width=900, min_height=680)


def edit_selected_sample(self) -> None:
    if not self.is_action_allowed("action.samples.edit"):
        return
    target_rows = self.get_target_sample_rows()
    if len(target_rows) != 1:
        messagebox.showwarning("Edit Sample", "Please select exactly one sample to edit.")
        return
    self.open_new_sample_dialog(target_rows[0])


def copy_selected_sample(self) -> None:
    if not self.is_action_allowed("action.samples.copy"):
        return
    target_rows = self.get_target_sample_rows()
    if len(target_rows) != 1:
        messagebox.showwarning("Copy Sample", "Please select exactly one sample to copy.")
        return
    self.open_new_sample_dialog(target_rows[0], duplicate=True)


def delete_selected_samples(self) -> None:
    if not self.is_action_allowed("action.samples.delete"):
        return
    target_rows = self.get_target_sample_rows()
    if not target_rows:
        messagebox.showwarning("Delete Sample", "Please tick or select at least one sample.")
        return
    if not messagebox.askyesno("Delete Sample", f"Delete {len(target_rows)} sample(s)?"):
        return
    target_ids = {str(row.get("record_id", "")).strip() for row in target_rows if str(row.get("record_id", "")).strip()}
    remaining_rows = [
        row
        for row in self.sample_rows
        if str(row.get("record_id", "")).strip() not in target_ids
    ]
    self.checked_sample_ids.difference_update(target_ids)
    self.sample_rows = self.enrich_sample_rows(self.save_sample_records_to_source(remaining_rows))
    self.apply_samples_filter()
    self.refresh_tech_pack_module(silent=True)
    self.set_status(f"Deleted {len(target_rows)} sample(s).")


def sync_tech_pack_from_samples(self) -> None:
    if not self.is_action_allowed("action.tool.download_tech_pack"):
        return
    selected_styles = self.get_target_sample_styles_for_sync()
    if not selected_styles:
        messagebox.showwarning("Sync Tech Pack", "Please tick or select at least one sample with a style.")
        return
    self.sync_tech_pack_styles(selected_styles)


def bind_samples_module(app_cls: type) -> None:
    method_map = {
        "_build_samples_module": _build_samples_module,
        "handle_samples_placeholder_action": handle_samples_placeholder_action,
        "refresh_samples_module": refresh_samples_module,
        "enrich_sample_rows": enrich_sample_rows,
        "apply_samples_filter": apply_samples_filter,
        "schedule_samples_filter_apply": schedule_samples_filter_apply,
        "run_samples_filter_apply": run_samples_filter_apply,
        "update_samples_filter_input_state": update_samples_filter_input_state,
        "refresh_samples_tree": refresh_samples_tree,
        "update_samples_tree_checkbox_state": update_samples_tree_checkbox_state,
        "get_selected_sample_row": get_selected_sample_row,
        "get_target_sample_rows": get_target_sample_rows,
        "get_target_sample_styles_for_sync": get_target_sample_styles_for_sync,
        "uncheck_sample_rows_for_style": uncheck_sample_rows_for_style,
        "on_samples_tree_click": on_samples_tree_click,
        "on_samples_tree_double_click": on_samples_tree_double_click,
        "sample_style_option_rows": sample_style_option_rows,
        "sample_pic_options_by_role": sample_pic_options_by_role,
        "sample_type_options_list": sample_type_options_list,
        "find_sample_style_option": find_sample_style_option,
        "resolve_sample_row_image": resolve_sample_row_image,
        "load_sample_cover_page_assets_from_source": load_sample_cover_page_assets_from_source,
        "save_sample_cover_page_assets_to_source": save_sample_cover_page_assets_to_source,
        "resolve_sample_cover_page_asset": resolve_sample_cover_page_asset,
        "make_sample_request_number": make_sample_request_number,
        "parse_sample_report_person_in_charge": parse_sample_report_person_in_charge,
        "parse_sample_report_sheet": parse_sample_report_sheet,
        "set_samples_report_import_running": set_samples_report_import_running,
        "rebuild_tech_pack_rows_from_current_state": rebuild_tech_pack_rows_from_current_state,
        "_run_samples_report_import_worker": _run_samples_report_import_worker,
        "finish_samples_report_import": finish_samples_report_import,
        "import_samples_report": import_samples_report,
        "commit_samples_search_query": commit_samples_search_query,
        "on_samples_search_submit": on_samples_search_submit,
        "sync_samples_search_query": sync_samples_search_query,
        "create_sample_dialog_preview_photo": create_sample_dialog_preview_photo,
        "create_sample_thumbnail": create_sample_thumbnail,
        "is_remote_image_reference_cached_only": is_remote_image_reference_cached_only,
        "sample_size_options": sample_size_options,
        "extract_tech_pack_colors": extract_tech_pack_colors,
        "get_sample_cover_page_baby_styles": get_sample_cover_page_baby_styles,
        "get_sample_cover_page_bom_selection": get_sample_cover_page_bom_selection,
        "build_sample_cover_page_file": build_sample_cover_page_file,
        "convert_sample_cover_page_to_pdf": convert_sample_cover_page_to_pdf,
        "open_add_sample_baby_style_dialog": open_add_sample_baby_style_dialog,
        "open_select_sample_bom_item_dialog": open_select_sample_bom_item_dialog,
        "open_new_sample_dialog": open_new_sample_dialog,
        "edit_selected_sample": edit_selected_sample,
        "copy_selected_sample": copy_selected_sample,
        "delete_selected_samples": delete_selected_samples,
        "sync_tech_pack_from_samples": sync_tech_pack_from_samples,
        "get_target_sample_cover_page_context": get_target_sample_cover_page_context,
        "sample_cover_page_collection_info": sample_cover_page_collection_info,
        "sample_cover_page_tech_pack_info": sample_cover_page_tech_pack_info,
        "build_sample_cover_page_items": build_sample_cover_page_items,
        "export_sample_cover_page": export_sample_cover_page,
        "print_sample_cover_page": print_sample_cover_page,
    }
    for method_name, function in method_map.items():
        setattr(app_cls, method_name, function)
