from __future__ import annotations

import re
import threading
import tkinter as tk
from datetime import datetime
from tkinter import filedialog, ttk
from tkinter import messagebox
from pathlib import Path

from PIL import Image, ImageChops, ImageFilter, ImageGrab, ImageOps, ImageStat, ImageTk
from app_core.filter_helpers import create_extra_filter_row, destroy_filter_row, regrid_filter_rows
from app_core.server_api import import_tech_pack_source_file
from app_core.tech_pack_service import run_download_tech_pack
from database import normalize_style_identifier

try:
    import windnd
except Exception:
    windnd = None

from app_core.ui_helpers import configure_striped_treeview, resolve_image_source

TECH_PACK_SORT_KEY_OPTIONS = [
    ("created_at", "create_time_column"),
    ("buyer", "buyer_name"),
    ("style_code", "tech_pack_style_column"),
    ("parent_summary", "tech_pack_parent_column"),
    ("season", "tech_pack_season_column"),
    ("po_summary", "tech_pack_po_column"),
    ("bom_summary", "tech_pack_bom_column"),
    ("construction_summary", "tech_pack_construction_column"),
    ("size_summary", "samples_size_column"),
    ("sample_summary", "tech_pack_sample_column"),
    ("production_summary", "tech_pack_production_column"),
    ("quality_summary", "tech_pack_quality_column"),
    ("updated_at", "revised_time_column"),
]

TECH_PACK_FILTER_FIELD_OPTIONS = [
    ("__all__", "samples_filter_all_option"),
    ("created_at", "create_time_column"),
    ("buyer", "buyer_name"),
    ("style_code", "tech_pack_style_column"),
    ("parent_summary", "tech_pack_parent_column"),
    ("season", "tech_pack_season_column"),
    ("po_summary", "tech_pack_po_column"),
    ("bom_summary", "tech_pack_bom_column"),
    ("construction_summary", "tech_pack_construction_column"),
    ("size_summary", "samples_size_column"),
    ("sample_summary", "tech_pack_sample_column"),
    ("production_summary", "tech_pack_production_column"),
    ("quality_summary", "tech_pack_quality_column"),
    ("updated_at", "revised_time_column"),
]

TECH_PACK_DATE_FILTER_FIELDS = {"created_at", "updated_at"}

TECH_PACK_COLUMNS = (
    "created_at",
    "select_flag",
    "buyer",
    "style_code",
    "parent_summary",
    "season",
    "po_summary",
    "bom_summary",
    "construction_summary",
    "size_summary",
    "sample_summary",
    "production_summary",
    "quality_summary",
    "updated_at",
    "latest_user",
)


def refresh_tech_pack_module(self, silent: bool = False) -> bool:
    # Always refresh the grouped Costing snapshot first so Tech Pack can surface
    # styles that exist in Costing even when they do not have Tech Pack records yet.
    self.load_saved_costing_imports(silent=True, refresh_tech_pack=False)
    new_library = self.load_tech_pack_library_from_source()
    new_hidden_styles = self.load_tech_pack_hidden_styles_from_source()
    self.tech_pack_library = new_library
    self.tech_pack_hidden_styles = new_hidden_styles
    new_rows = self.build_tech_pack_rows()
    changed = new_rows != self.tech_pack_rows
    if changed or not silent:
        self.tech_pack_rows = new_rows
        self.invalidate_style_season_lookup_cache()
        if hasattr(self, "refresh_purchase_order_seasons_from_tech_pack"):
            self.refresh_purchase_order_seasons_from_tech_pack()
        self.apply_tech_pack_filter()
    elif hasattr(self, "refresh_purchase_order_seasons_from_tech_pack"):
        self.refresh_purchase_order_seasons_from_tech_pack()
    self.update_tech_pack_module_status()
    if hasattr(self, "save_main_ui_session_cache"):
        self.save_main_ui_session_cache(
            "tech_pack",
            {
                "costing_data": getattr(self, "costing_data", None),
                "costing_items": list(getattr(self, "costing_items", []) or []),
                "tech_pack_library": list(getattr(self, "tech_pack_library", []) or []),
                "tech_pack_hidden_styles": sorted(getattr(self, "tech_pack_hidden_styles", set()) or []),
                "tech_pack_rows": list(getattr(self, "tech_pack_rows", []) or []),
            },
        )
    return changed


TECH_PACK_IMAGE_SEARCH_TEXT = {
    "en": {
        "button": "Search by image",
        "title": "Search Tech Pack by Image",
        "input_title": "Find similar styles from one image",
        "input_hint": "Choose one method below to load the reference image.",
        "paste": "Paste Ctrl+V",
        "browse": "Open file",
        "path": "Open path",
        "search_now": "Search now",
        "drop_here": "Drag and drop an image file here",
        "drop_hint": "Supported: PNG, JPG, JPEG, BMP, GIF, WEBP, TIF, TIFF",
        "path_placeholder": "Paste an image path here, then click Open path.",
        "source_ready": "Loaded image: {name}",
        "source_missing": "Please paste, drop, or open an image first.",
        "path_invalid": "The selected path is not a valid image file.",
        "image_invalid": "The selected image could not be processed.",
        "clipboard_empty": "Clipboard does not contain an image.",
        "clipboard_invalid": "Clipboard data is not a supported image.",
        "no_library": "Tech Pack library does not have cover images yet.",
        "no_match": "No similar Tech Pack cover image was found.",
        "results": "Similar styles",
        "similarity": "Match %",
        "open": "Open Selected",
        "status": "{count} style(s) matched the pasted image.",
        "progress": "Searching similar Tech Pack cover images...",
        "style_name": "Style Name",
    },
    "vi": {
        "button": "Tim bang anh",
        "title": "Tim Tech Pack bang anh",
        "input_title": "Tim style tuong tu tu 1 hinh tham chieu",
        "input_hint": "Chon 1 trong cac cach duoi day de nap anh tham chieu.",
        "paste": "Paste Ctrl+V",
        "browse": "Mo file",
        "path": "Mo duong dan",
        "search_now": "Bat dau tim",
        "drop_here": "Keo va tha file anh vao day",
        "drop_hint": "Ho tro: PNG, JPG, JPEG, BMP, GIF, WEBP, TIF, TIFF",
        "path_placeholder": "Dan duong dan anh vao day, sau do bam Mo duong dan.",
        "source_ready": "Da nap anh: {name}",
        "source_missing": "Hay paste, keo tha, hoac mo 1 anh truoc.",
        "path_invalid": "Duong dan da chon khong phai file anh hop le.",
        "image_invalid": "Khong the xu ly anh da chon.",
        "clipboard_empty": "Clipboard hien khong co anh.",
        "clipboard_invalid": "Du lieu trong clipboard khong phai anh hop le.",
        "no_library": "Thu vien Tech Pack hien chua co anh bia de doi chieu.",
        "no_match": "Khong tim thay style co anh tuong doi giong.",
        "results": "Cac style tuong tu",
        "similarity": "Do giong %",
        "open": "Mo style da chon",
        "status": "Da tim thay {count} style giong voi anh vua paste.",
        "progress": "Dang tim cac anh bia Tech Pack tuong tu...",
        "style_name": "Ten style",
    },
}


def get_tech_pack_image_search_text(self, key: str, **kwargs: object) -> str:
    mapping = {
        "button": "tech_pack_search_image_button",
        "title": "tech_pack_search_image_title",
        "clipboard_empty": "tech_pack_search_image_clipboard_empty",
        "clipboard_invalid": "tech_pack_search_image_clipboard_invalid",
        "no_library": "tech_pack_search_image_no_library",
        "no_match": "tech_pack_search_image_no_match",
        "results": "tech_pack_search_image_results",
        "similarity": "tech_pack_search_image_similarity",
        "open": "tech_pack_search_image_open",
        "status": "tech_pack_search_image_status",
        "progress": "tech_pack_search_image_progress",
    }
    text_key = mapping.get(key)
    if text_key:
        translated = self.t(text_key, **kwargs)
        if translated != text_key:
            return translated
    language = getattr(self, "current_language", "en")
    fallback = TECH_PACK_IMAGE_SEARCH_TEXT.get(language, TECH_PACK_IMAGE_SEARCH_TEXT["en"]).get(
        key,
        TECH_PACK_IMAGE_SEARCH_TEXT["en"].get(key, key),
    )
    return str(fallback).format(**kwargs)


def _build_tech_pack_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["tech_pack_import_button"] = ttk.Button(toolbar, command=self.import_tech_pack_pdf)
    self.widget_text["tech_pack_import_button"].pack(side="left")
    self.register_permission_widget("action.tech_pack.import_pdf", self.widget_text["tech_pack_import_button"])
    self.widget_text["tech_pack_import_folder_button"] = ttk.Button(toolbar, command=self.import_tech_pack_folder)
    self.widget_text["tech_pack_import_folder_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.tech_pack.import_folder", self.widget_text["tech_pack_import_folder_button"])
    self.widget_text["tech_pack_export_button"] = ttk.Button(toolbar, command=self.export_tech_pack_report)
    self.widget_text["tech_pack_export_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.tech_pack.export_report", self.widget_text["tech_pack_export_button"])
    self.widget_text["tech_pack_add_style_button"] = ttk.Button(toolbar, command=self.open_add_tech_pack_style_dialog)
    self.widget_text["tech_pack_add_style_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.tech_pack.edit_style", self.widget_text["tech_pack_add_style_button"])
    self.widget_text["tech_pack_delete_button"] = ttk.Button(toolbar, command=self.delete_selected_tech_pack_styles)
    self.widget_text["tech_pack_delete_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.tech_pack.delete_style", self.widget_text["tech_pack_delete_button"])
    self.widget_text["tech_pack_edit_button"] = ttk.Button(toolbar, command=self.edit_selected_tech_pack_row)
    self.widget_text["tech_pack_edit_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.tech_pack.edit_style", self.widget_text["tech_pack_edit_button"])
    self.widget_text["tech_pack_sync_button"] = ttk.Button(toolbar, command=self.sync_selected_tech_pack_styles)
    self.widget_text["tech_pack_sync_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.tool.download_tech_pack", self.widget_text["tech_pack_sync_button"])
    self.widget_text["tech_pack_search_image_button"] = ttk.Button(
        toolbar,
        text=self.get_tech_pack_image_search_text("button"),
        command=self.search_tech_pack_by_image,
    )
    self.widget_text["tech_pack_search_image_button"].pack(side="left", padx=(8, 0))
    self.widget_text["tech_pack_edit_season_button"] = ttk.Button(
        toolbar,
        text="Edit season",
        command=self.open_edit_selected_tech_pack_season_dialog,
    )
    self.widget_text["tech_pack_edit_season_button"].pack(side="left", padx=(8, 0))
    self.register_permission_widget("action.tech_pack.edit_style", self.widget_text["tech_pack_edit_season_button"])

    meta = ttk.Frame(parent)
    meta.grid(row=1, column=0, sticky="ew", pady=(12, 14))
    meta.columnconfigure(14, weight=1)
    self.tech_pack_filter_controls = meta
    self.tech_pack_filter_rows = []
    meta.columnconfigure(3, weight=0)
    meta.columnconfigure(5, weight=0)
    self.widget_text["tech_pack_filter_field_label"] = ttk.Label(meta)
    self.widget_text["tech_pack_filter_field_label"].grid(row=0, column=0, sticky="w", padx=(0, 6))
    self.tech_pack_filter_field_combo = ttk.Combobox(meta, textvariable=self.tech_pack_filter_field_var, state="readonly", width=22)
    self.tech_pack_filter_field_combo.grid(row=0, column=1, sticky="w")
    self.tech_pack_filter_field_combo.bind("<<ComboboxSelected>>", self.on_tech_pack_filter_field_changed)
    self.widget_text["tech_pack_filter_value_label"] = ttk.Label(meta)
    self.widget_text["tech_pack_filter_value_label"].grid(row=0, column=2, sticky="w", padx=(12, 6))
    self.tech_pack_search_entry = ttk.Entry(meta, textvariable=self.tech_pack_search_var, width=36)
    self.tech_pack_search_entry.grid(row=0, column=3, sticky="w")
    self.tech_pack_search_entry.bind("<Return>", self.on_tech_pack_search_submit)
    self.tech_pack_search_entry.bind("<KP_Enter>", self.on_tech_pack_search_submit)
    self.widget_text["tech_pack_date_from_label"] = ttk.Label(meta)
    self.widget_text["tech_pack_date_from_label"].grid(row=0, column=4, sticky="w", padx=(12, 6))
    self.tech_pack_date_from_entry = ttk.Entry(meta, textvariable=self.tech_pack_date_from_var, width=12)
    self.tech_pack_date_from_entry.grid(row=0, column=5, sticky="w", padx=(0, 12))
    self.bind_date_entry_picker(self.tech_pack_date_from_entry, self.tech_pack_date_from_var)
    self.tech_pack_date_from_entry.bind("<Return>", self.on_tech_pack_search_submit)
    self.tech_pack_date_from_entry.bind("<KP_Enter>", self.on_tech_pack_search_submit)
    self.widget_text["tech_pack_date_to_label"] = ttk.Label(meta)
    self.widget_text["tech_pack_date_to_label"].grid(row=0, column=6, sticky="w", padx=(0, 6))
    self.tech_pack_date_to_entry = ttk.Entry(meta, textvariable=self.tech_pack_date_to_var, width=12)
    self.tech_pack_date_to_entry.grid(row=0, column=7, sticky="w")
    self.bind_date_entry_picker(self.tech_pack_date_to_entry, self.tech_pack_date_to_var)
    self.tech_pack_date_to_entry.bind("<Return>", self.on_tech_pack_search_submit)
    self.tech_pack_date_to_entry.bind("<KP_Enter>", self.on_tech_pack_search_submit)
    self.widget_text["tech_pack_sort_by_label"] = ttk.Label(meta)
    self.widget_text["tech_pack_sort_by_label"].grid(row=0, column=8, sticky="w", padx=(12, 6))
    self.tech_pack_sort_key_combo = ttk.Combobox(
        meta,
        textvariable=self.tech_pack_sort_key_var,
        state="readonly",
        width=20,
    )
    self.tech_pack_sort_key_combo.grid(row=0, column=9, sticky="w")
    self.tech_pack_sort_key_combo.bind("<<ComboboxSelected>>", self.on_tech_pack_sort_key_changed)
    self.widget_text["tech_pack_sort_order_label"] = ttk.Label(meta)
    self.widget_text["tech_pack_sort_order_label"].grid(row=0, column=10, sticky="w", padx=(12, 6))
    self.tech_pack_sort_order_combo = ttk.Combobox(
        meta,
        textvariable=self.tech_pack_sort_order_var,
        state="readonly",
        width=12,
    )
    self.tech_pack_sort_order_combo.grid(row=0, column=11, sticky="w")
    self.tech_pack_sort_order_combo.bind("<<ComboboxSelected>>", self.on_tech_pack_sort_order_changed)
    self.widget_text["tech_pack_add_filter_button"] = ttk.Button(meta, text=self.t("po_add_filter"), command=self.add_tech_pack_filter_row)
    self.widget_text["tech_pack_add_filter_button"].grid(row=0, column=12, sticky="w", padx=(12, 0))
    self.widget_text["tech_pack_clear_filters_button"] = ttk.Button(meta, text=self.t("po_clear_filters"), command=self.clear_tech_pack_filters)
    self.widget_text["tech_pack_clear_filters_button"].grid(row=0, column=13, sticky="w", padx=(8, 0))
    self.update_tech_pack_filter_input_state()

    self.widget_text["tech_pack_styles_section"] = ttk.Label(parent, style="Section.TLabel")
    self.widget_text["tech_pack_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.tech_pack_tree = ttk.Treeview(
        table_frame,
        columns=TECH_PACK_COLUMNS,
        show="tree headings",
        selectmode="browse",
        style="Costing.Treeview",
    )
    self.tech_pack_tree.column("#0", width=96, anchor="center", stretch=False)
    self.tech_pack_tree.column("select_flag", width=60, anchor="center", stretch=False)
    self.tech_pack_tree.column("created_at", width=145, anchor="center", stretch=False)
    self.tech_pack_tree.column("buyer", width=90, anchor="w", stretch=False)
    self.tech_pack_tree.column("style_code", width=110, anchor="w", stretch=False)
    self.tech_pack_tree.column("parent_summary", width=130, anchor="w", stretch=False)
    self.tech_pack_tree.column("season", width=110, anchor="center", stretch=False)
    self.tech_pack_tree.column("po_summary", width=180, anchor="w", stretch=False)
    self.tech_pack_tree.column("bom_summary", width=180, anchor="w", stretch=False)
    self.tech_pack_tree.column("construction_summary", width=180, anchor="w", stretch=False)
    self.tech_pack_tree.column("size_summary", width=150, anchor="w", stretch=False)
    self.tech_pack_tree.column("sample_summary", width=160, anchor="w", stretch=False)
    self.tech_pack_tree.column("production_summary", width=180, anchor="w", stretch=False)
    self.tech_pack_tree.column("quality_summary", width=160, anchor="w", stretch=False)
    self.tech_pack_tree.column("updated_at", width=145, anchor="center", stretch=False)
    self.tech_pack_tree.column("latest_user", width=140, anchor="w", stretch=False)
    self.tech_pack_tree.heading("#0", command=lambda: None)
    for column_key in TECH_PACK_COLUMNS:
        self.tech_pack_tree.heading(column_key, command=lambda key=column_key: self.on_tech_pack_heading_click(key))
    self.tech_pack_tree.heading("latest_user", text=self.t("latest_user_column"), command=lambda: self.on_tech_pack_heading_click("latest_user"))
    tech_pack_scroll = ttk.Scrollbar(table_frame, orient="vertical", command=self.tech_pack_tree.yview)
    tech_pack_scroll_x = ttk.Scrollbar(table_frame, orient="horizontal", command=self.tech_pack_tree.xview)
    self.tech_pack_tree.configure(yscrollcommand=tech_pack_scroll.set, xscrollcommand=tech_pack_scroll_x.set)
    configure_striped_treeview(self.tech_pack_tree)
    self.tech_pack_tree.grid(row=0, column=0, sticky="nsew")
    tech_pack_scroll.grid(row=0, column=1, sticky="ns")
    tech_pack_scroll_x.grid(row=1, column=0, sticky="ew")
    self.tech_pack_tree.bind("<<TreeviewSelect>>", self.on_tech_pack_item_selected)
    self.tech_pack_tree.bind("<Button-1>", self.on_tech_pack_tree_click)
    self.tech_pack_tree.bind("<Double-1>", self.on_tech_pack_item_double_click)

    self.tech_pack_status_label = ttk.Label(
        parent,
        textvariable=self.status_var,
        relief="sunken",
        anchor="w",
        justify="left",
        padding=8,
        wraplength=1180,
    )
    self.tech_pack_status_label.grid(row=4, column=0, sticky="ew", pady=(12, 0))


def apply_tech_pack_filter(self) -> None:
    if getattr(self, "_tech_pack_filter_update_suspended", False):
        return
    self.filtered_tech_pack_rows = list(self.tech_pack_rows)
    active_filters = self.get_active_tech_pack_filters()
    if active_filters:
        self.filtered_tech_pack_rows = [
            row for row in self.filtered_tech_pack_rows if all(self.tech_pack_row_matches_filter(row, item) for item in active_filters)
        ]

    sort_key = self.tech_pack_sort_key_internal
    reverse = self.tech_pack_sort_order_internal == "desc"

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

    self.filtered_tech_pack_rows.sort(key=sort_value, reverse=reverse)
    if hasattr(self, "tech_pack_tree"):
        self.refresh_tech_pack_tree()
    self.update_tech_pack_module_status()


def commit_tech_pack_search_query(self, value: object | None = None, *, apply_filter: bool = True) -> None:
    committed_value = self.tech_pack_search_var.get() if value is None else value
    self.tech_pack_filter_field_committed_internal = getattr(self, "tech_pack_filter_field_internal", "__all__")
    self.tech_pack_search_committed_value = str(committed_value or "").strip()
    self.tech_pack_date_from_committed_value = self.tech_pack_date_from_var.get().strip()
    self.tech_pack_date_to_committed_value = self.tech_pack_date_to_var.get().strip()
    for row_data in list(getattr(self, "tech_pack_filter_rows", []) or []):
        row_data["committed_field_internal"] = str(row_data.get("field_internal", "__all__") or "__all__")
        value_var = row_data.get("value_var")
        from_var = row_data.get("from_var")
        to_var = row_data.get("to_var")
        row_data["committed_query"] = value_var.get().strip() if value_var is not None else ""
        row_data["committed_from"] = from_var.get().strip() if from_var is not None else ""
        row_data["committed_to"] = to_var.get().strip() if to_var is not None else ""
    if apply_filter:
        self.apply_tech_pack_filter()


def tech_pack_filter_field_value(self, row: dict, field: str) -> object:
    if field == "updated_at":
        return row.get("updated_at") or row.get("last_updated")
    return row.get(field)


def tech_pack_row_matches_filter(self, row: dict, filter_item: dict) -> bool:
    field = str(filter_item.get("field", "__all__") or "__all__")
    query = str(filter_item.get("query", "") or "").strip().lower()
    from_text = str(filter_item.get("from", "") or "").strip()
    to_text = str(filter_item.get("to", "") or "").strip()
    if field in TECH_PACK_DATE_FILTER_FIELDS and (from_text or to_text):
        return self.is_within_date_range(self.tech_pack_filter_field_value(row, field), from_text, to_text)
    if not query:
        return True
    if field == "__all__":
        fields = [
            "buyer", "style_code", "style_identity", "parent_summary", "season", "po_summary", "bom_summary",
            "construction_summary", "size_summary", "sample_summary", "production_summary", "quality_summary",
            "collection", "factory", "created_at", "updated_at",
        ]
        return query in " ".join(self.display_value(self.tech_pack_filter_field_value(row, key)) for key in fields).lower()
    return query in self.display_value(self.tech_pack_filter_field_value(row, field)).strip().lower()


def get_active_tech_pack_filters(self) -> list[dict]:
    active: list[dict] = []
    field = str(getattr(self, "tech_pack_filter_field_committed_internal", getattr(self, "tech_pack_filter_field_internal", "__all__")) or "__all__")
    query = str(getattr(self, "tech_pack_search_committed_value", "") or "").strip()
    from_text = str(getattr(self, "tech_pack_date_from_committed_value", "") or "").strip()
    to_text = str(getattr(self, "tech_pack_date_to_committed_value", "") or "").strip()
    if field in TECH_PACK_DATE_FILTER_FIELDS:
        if from_text or to_text:
            active.append({"field": field, "from": from_text, "to": to_text})
    elif query:
        active.append({"field": field, "query": query})
    for row_data in list(getattr(self, "tech_pack_filter_rows", []) or []):
        row_field = str(row_data.get("committed_field_internal", row_data.get("field_internal", "__all__")) or "__all__")
        row_query = str(row_data.get("committed_query", "") or "").strip()
        row_from = str(row_data.get("committed_from", "") or "").strip()
        row_to = str(row_data.get("committed_to", "") or "").strip()
        if row_field in TECH_PACK_DATE_FILTER_FIELDS:
            if row_from or row_to:
                active.append({"field": row_field, "from": row_from, "to": row_to})
        elif row_query:
            active.append({"field": row_field, "query": row_query})
    return active


def add_tech_pack_filter_row(self) -> None:
    rows = getattr(self, "tech_pack_filter_rows", [])
    row_data = create_extra_filter_row(
        self,
        module_key="tech_pack",
        parent=self.tech_pack_filter_controls,
        options=TECH_PACK_FILTER_FIELD_OPTIONS,
        date_fields=TECH_PACK_DATE_FILTER_FIELDS,
        row_index=len(rows) + 1,
        remove_command=self.remove_tech_pack_filter_row,
        submit_command=self.on_tech_pack_search_submit,
        translate_filter_field=self.translate_tech_pack_filter_field,
    )
    rows.append(row_data)
    self.tech_pack_filter_rows = rows


def remove_tech_pack_filter_row(self, row_data: dict) -> None:
    rows = [row for row in list(getattr(self, "tech_pack_filter_rows", []) or []) if row is not row_data]
    destroy_filter_row(row_data)
    self.tech_pack_filter_rows = rows
    regrid_filter_rows(rows)
    self.apply_tech_pack_filter()


def clear_tech_pack_filters(self) -> None:
    self._tech_pack_filter_update_suspended = True
    try:
        self.tech_pack_filter_field_internal = "__all__"
        self.tech_pack_filter_field_committed_internal = "__all__"
        self.tech_pack_filter_field_var.set(self.translate_tech_pack_filter_field("__all__"))
        self.sync_tech_pack_search_query("")
        self.tech_pack_date_from_committed_value = ""
        self.tech_pack_date_to_committed_value = ""
        self.tech_pack_date_from_var.set("")
        self.tech_pack_date_to_var.set("")
        for row_data in list(getattr(self, "tech_pack_filter_rows", []) or []):
            destroy_filter_row(row_data)
        self.tech_pack_filter_rows = []
    finally:
        self._tech_pack_filter_update_suspended = False
    self.update_tech_pack_filter_input_state()
    self.apply_tech_pack_filter()


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


def sync_tech_pack_search_query(self, value: object) -> None:
    normalized_value = str(value or "").strip()
    self.tech_pack_search_var.set(normalized_value)
    self.tech_pack_search_committed_value = normalized_value


def schedule_tech_pack_filter_apply(self) -> None:
    self.commit_tech_pack_search_query()


def run_tech_pack_filter_apply(self) -> None:
    self.apply_tech_pack_filter()


def update_tech_pack_filter_input_state(self, *, clear_irrelevant: bool = False) -> None:
    filter_field = getattr(self, "tech_pack_filter_field_internal", "__all__")
    is_date_field = filter_field in TECH_PACK_DATE_FILTER_FIELDS
    if hasattr(self, "tech_pack_search_entry"):
        self.tech_pack_search_entry.configure(state="disabled" if is_date_field else "normal")
    if hasattr(self, "tech_pack_date_from_entry"):
        self.tech_pack_date_from_entry.configure(state="normal" if is_date_field else "disabled")
    if hasattr(self, "tech_pack_date_to_entry"):
        self.tech_pack_date_to_entry.configure(state="normal" if is_date_field else "disabled")
    if clear_irrelevant:
        self._tech_pack_filter_update_suspended = True
        try:
            if is_date_field:
                self.sync_tech_pack_search_query("")
            else:
                self.tech_pack_date_from_var.set("")
                self.tech_pack_date_to_var.set("")
        finally:
            self._tech_pack_filter_update_suspended = False


def refresh_tech_pack_tree(self, selected_id: str | None = None) -> None:
    current_selection = self.tech_pack_tree.selection() if hasattr(self, "tech_pack_tree") else ()
    if selected_id is None and current_selection:
        selected_id = current_selection[0]
    for item_id in self.tech_pack_tree.get_children():
        self.tech_pack_tree.delete(item_id)
    self.tech_pack_thumbnail_photos = []
    thumbnail_limit = self.get_startup_thumbnail_render_limit("tech_pack") if hasattr(self, "get_startup_thumbnail_render_limit") else None

    for index, row in enumerate(self.filtered_tech_pack_rows, start=1):
        thumbnail_photo = None
        if thumbnail_limit is None or index <= thumbnail_limit:
            thumbnail_photo = self.create_tech_pack_thumbnail(row)
            if thumbnail_photo is not None:
                self.tech_pack_thumbnail_photos.append(thumbnail_photo)
        row_values = (
            self.display_value(row.get("created_at")),
            self.get_tech_pack_check_display(row),
            self.display_value(row.get("buyer")),
            self.display_value(row.get("style_code")),
            self.display_value(row.get("parent_summary")),
            self.display_value(row.get("season")),
            self.display_value(row.get("po_summary")),
            self.display_value(row.get("bom_summary")),
            self.display_value(row.get("construction_summary")),
            self.display_value(row.get("size_summary")),
            self.display_value(row.get("sample_summary")),
            self.display_value(row.get("production_summary")),
            self.display_value(row.get("quality_summary")),
            self.display_value(row.get("updated_at") or row.get("last_updated")),
            self.display_value(row.get("latest_user")),
        )
        insert_kwargs = {
            "text": "",
            "values": row_values,
            "tags": ("odd_row" if index % 2 == 1 else "even_row",),
        }
        if thumbnail_photo is not None:
            insert_kwargs["image"] = thumbnail_photo
        self.tech_pack_tree.insert("", "end", str(index), **insert_kwargs)

    valid_styles = {str(row.get("style_code", "")).strip() for row in self.tech_pack_rows}
    self.checked_tech_pack_styles.intersection_update(valid_styles)

    if selected_id and self.tech_pack_tree.exists(selected_id):
        self.tech_pack_tree.selection_set(selected_id)
        self.tech_pack_tree.focus(selected_id)
        self.on_tech_pack_item_selected(None)
    elif self.filtered_tech_pack_rows:
        self.tech_pack_tree.selection_set("1")
        self.tech_pack_tree.focus("1")
        self.tech_pack_selected_index = 0
    else:
        self.tech_pack_selected_index = None
    if hasattr(self, "note_startup_thumbnail_render_state"):
        self.note_startup_thumbnail_render_state(
            "tech_pack",
            total_rows=len(self.filtered_tech_pack_rows),
            rendered_rows=min(len(self.filtered_tech_pack_rows), thumbnail_limit) if thumbnail_limit is not None else len(self.filtered_tech_pack_rows),
        )
    self.update_tech_pack_module_status()


def update_tech_pack_tree_check_cell(self, row_id: str | None = None) -> None:
    if not hasattr(self, "tech_pack_tree"):
        return
    target_ids = [row_id] if row_id else list(self.tech_pack_tree.get_children())
    for item_id in target_ids:
        if not self.tech_pack_tree.exists(item_id):
            continue
        try:
            index = int(item_id) - 1
        except Exception:
            continue
        if index < 0 or index >= len(self.filtered_tech_pack_rows):
            continue
        row = self.filtered_tech_pack_rows[index]
        values = list(self.tech_pack_tree.item(item_id, "values"))
        if len(values) < len(TECH_PACK_COLUMNS):
            continue
        values[1] = self.get_tech_pack_check_display(row)
        self.tech_pack_tree.item(item_id, values=tuple(values))
    self.update_tech_pack_module_status()


def on_tech_pack_heading_click(self, column: str) -> None:
    if column != "select_flag":
        return
    visible_styles = [
        str(row.get("style_code", "")).strip()
        for row in self.filtered_tech_pack_rows
        if str(row.get("style_code", "")).strip()
    ]
    checked_visible_styles = {
        style_code
        for style_code in visible_styles
        if style_code in self.checked_tech_pack_styles
    }
    if checked_visible_styles and len(checked_visible_styles) == len(visible_styles):
        self.checked_tech_pack_styles.difference_update(visible_styles)
    else:
        self.checked_tech_pack_styles.update(visible_styles)
    self.update_tech_pack_tree_check_cell()


def on_tech_pack_tree_click(self, event: tk.Event) -> str | None:
    region = self.tech_pack_tree.identify_region(event.x, event.y)
    if region != "cell":
        return None
    column = self.tech_pack_tree.identify_column(event.x)
    if column != "#2":
        return None
    row_id = self.tech_pack_tree.identify_row(event.y)
    if not row_id:
        return None
    index = int(row_id) - 1
    if index < 0 or index >= len(self.filtered_tech_pack_rows):
        return "break"
    style_code = str(self.filtered_tech_pack_rows[index].get("style_code", "")).strip()
    if style_code in self.checked_tech_pack_styles:
        self.checked_tech_pack_styles.remove(style_code)
    else:
        self.checked_tech_pack_styles.add(style_code)
    self.update_tech_pack_tree_check_cell(row_id)
    return "break"


def uncheck_tech_pack_style(self, style_code: str) -> None:
    normalized_style = str(style_code).strip()
    if not normalized_style:
        return
    if normalized_style not in self.checked_tech_pack_styles:
        return
    self.checked_tech_pack_styles.discard(normalized_style)
    for index, row in enumerate(self.filtered_tech_pack_rows, start=1):
        if str(row.get("style_code", "")).strip() == normalized_style:
            self.update_tech_pack_tree_check_cell(str(index))
            return
    self.update_tech_pack_module_status()


def on_tech_pack_item_selected(self, _event: object) -> None:
    selection = self.tech_pack_tree.selection()
    if not selection:
        return
    index = int(selection[0]) - 1
    if 0 <= index < len(self.filtered_tech_pack_rows):
        self.tech_pack_selected_index = index
    self.update_tech_pack_module_status()


def on_tech_pack_item_double_click(self, event: object) -> None:
    row = None
    row_id = ""
    if isinstance(event, tk.Event):
        row_id = self.tech_pack_tree.identify_row(event.y)
    if not row_id:
        selection = self.tech_pack_tree.selection()
        if selection:
            row_id = selection[0]
    if row_id:
        index = int(row_id) - 1
        if 0 <= index < len(self.filtered_tech_pack_rows):
            row = self.filtered_tech_pack_rows[index]
            self.tech_pack_selected_index = index
    if row is None:
        row = self.get_selected_tech_pack_row()
    if row is not None:
        self.open_tech_pack_detail_window(row)


def get_selected_tech_pack_row(self) -> dict | None:
    if self.tech_pack_selected_index is None:
        return None
    if self.tech_pack_selected_index < 0 or self.tech_pack_selected_index >= len(self.filtered_tech_pack_rows):
        return None
    return self.filtered_tech_pack_rows[self.tech_pack_selected_index]


def collect_existing_tech_pack_season_options(self) -> list[str]:
    seasons: dict[str, str] = {}

    def clean_season_option(value: object, year_value: object = "") -> str:
        season_text = str(value or "").strip()
        if hasattr(self, "split_season_display"):
            season_text, _parsed_year = self.split_season_display(season_text, year_value)
        season_text = str(season_text or "").strip()
        match = re.match(r"^(?P<season>.+?)[\s\-/]+(?P<year>\d{2}|\d{4})$", season_text)
        if match:
            season_text = match.group("season").strip()
        return season_text

    def push(value: object, year_value: object = "") -> None:
        season_text = clean_season_option(value, year_value)
        if not season_text or season_text in {"-", "--", "N/A", "n/a"}:
            return
        seasons.setdefault(season_text.lower(), season_text)

    for row in list(getattr(self, "tech_pack_rows", []) or []):
        push(row.get("season_base") or row.get("season"), row.get("season_year"))
    for row in list(getattr(self, "tech_pack_library", []) or []):
        overview_data = row.get("overview_data", {})
        if not isinstance(overview_data, dict):
            overview_data = {}
        push(overview_data.get("season") or row.get("season"), overview_data.get("season_year") or row.get("season_year"))
    for row in list(getattr(self, "costing_items", []) or []):
        push(row.get("season"), row.get("season_year"))
    for row in list(getattr(self, "all_orders", []) or []):
        push(row.get("season"), row.get("season_year"))
    return sorted(seasons.values(), key=lambda item: item.upper())


def open_add_tech_pack_style_dialog(self) -> None:
    if not self.is_action_allowed("action.tech_pack.edit_style"):
        return
    dialog = tk.Toplevel(self.root)
    dialog.title("Add style")
    dialog.transient(self.root)
    dialog.grab_set()
    dialog.minsize(420, 300)

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

    ttk.Label(
        frame,
        text="Add style",
        style="Section.TLabel",
    ).grid(row=0, column=0, sticky="w")
    ttk.Label(
        frame,
        text="Style",
    ).grid(row=1, column=0, sticky="w", pady=(14, 6))

    style_var = tk.StringVar()
    style_entry = ttk.Entry(frame, textvariable=style_var)
    style_entry.grid(row=2, column=0, sticky="ew")

    ttk.Label(frame, text="Season").grid(row=3, column=0, sticky="w", pady=(12, 6))
    season_options = self.collect_existing_tech_pack_season_options()
    season_var = tk.StringVar(value=season_options[0] if season_options else "")
    season_combo = ttk.Combobox(frame, textvariable=season_var, values=season_options, state="readonly")
    season_combo.grid(row=4, column=0, sticky="ew")

    ttk.Label(frame, text="Season Year").grid(row=5, column=0, sticky="w", pady=(12, 6))
    season_year_var = tk.StringVar()
    season_year_entry = ttk.Entry(frame, textvariable=season_year_var)
    season_year_entry.grid(row=6, column=0, sticky="ew")

    status_var = tk.StringVar(value="")
    ttk.Label(frame, textvariable=status_var, foreground="#8a4b1f").grid(
        row=7,
        column=0,
        sticky="w",
        pady=(10, 0),
    )

    def save_style() -> None:
        raw_style = style_var.get().strip()
        normalized_style = normalize_style_identifier(raw_style)
        if not normalized_style:
            status_var.set("Please enter a style.")
            style_entry.focus_set()
            return
        existing_styles = {
            normalize_style_identifier(row.get("style_code"))
            for row in self.tech_pack_rows
        }
        if normalized_style in existing_styles:
            status_var.set(f"Style {normalized_style} already exists.")
            style_entry.focus_set()
            style_entry.selection_range(0, tk.END)
            return

        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        new_record = self.default_tech_pack_record(normalized_style)
        if hasattr(self, "split_season_display"):
            season_base, season_year = self.split_season_display(season_var.get(), season_year_var.get())
        else:
            season_base = str(season_var.get() or "").strip()
            season_year = str(season_year_var.get() or "").strip()
        if hasattr(self, "normalize_season_year"):
            season_year = self.normalize_season_year(season_year)
        season_display = self.build_season_display(season_base, season_year) if hasattr(self, "build_season_display") else season_base
        style_identity = self.build_style_identity(normalized_style, season_base, season_year) if hasattr(self, "build_style_identity") else normalized_style
        new_record["season"] = season_base
        new_record["season_year"] = season_year
        new_record["season_display"] = season_display
        new_record["style_identity"] = style_identity
        new_record["overview_data"] = {
            **(new_record.get("overview_data") if isinstance(new_record.get("overview_data"), dict) else {}),
            "season": season_base,
            "season_year": season_year,
        }
        new_record["created_at"] = timestamp
        new_record["updated_at"] = timestamp
        new_record["last_updated"] = timestamp
        saved_record = self.save_tech_pack_record_to_source(new_record)
        saved_style = normalize_style_identifier(saved_record.get("style_code")) or normalized_style
        saved_record["style_code"] = saved_style
        library_by_style = {
            str(item.get("style_code", "")).strip(): dict(item)
            for item in self.tech_pack_library
            if str(item.get("style_code", "")).strip()
        }
        library_by_style[saved_style] = saved_record
        self.tech_pack_library = list(library_by_style.values())
        hidden_styles = {
            normalize_style_identifier(item)
            for item in set(getattr(self, "tech_pack_hidden_styles", set()) or set())
            if normalize_style_identifier(item)
        }
        if saved_style in hidden_styles:
            hidden_styles.discard(saved_style)
            if hasattr(self, "save_tech_pack_hidden_styles_to_source"):
                self.tech_pack_hidden_styles = self.save_tech_pack_hidden_styles_to_source(hidden_styles)
            else:
                self.tech_pack_hidden_styles = hidden_styles
        self.tech_pack_rows = self.build_tech_pack_rows()
        if hasattr(self, "invalidate_style_season_lookup_cache"):
            self.invalidate_style_season_lookup_cache()
        self.apply_tech_pack_filter()
        if hasattr(self, "sync_costing_item_seasons_from_tech_pack") and self.sync_costing_item_seasons_from_tech_pack() and hasattr(self, "costing_tree"):
            self.apply_costing_filter()
        if hasattr(self, "save_main_ui_session_cache"):
            self.save_main_ui_session_cache(
                "tech_pack",
                {
                    "costing_data": getattr(self, "costing_data", None),
                    "costing_items": list(getattr(self, "costing_items", []) or []),
                    "tech_pack_library": list(getattr(self, "tech_pack_library", []) or []),
                    "tech_pack_hidden_styles": sorted(getattr(self, "tech_pack_hidden_styles", set()) or []),
                    "tech_pack_rows": list(getattr(self, "tech_pack_rows", []) or []),
                },
            )
        self.focus_tech_pack_style_in_list(saved_style)
        self.set_status(f"Added Tech Pack style {saved_style}.")
        dialog.destroy()

    actions = ttk.Frame(frame)
    actions.grid(row=8, column=0, sticky="e", pady=(16, 0))
    ttk.Button(actions, text=self.t("close"), command=dialog.destroy).pack(side="left", padx=(0, 8))
    save_button = ttk.Button(actions, text=self.t("collection_list_save"), command=save_style)
    save_button.pack(side="left")
    self.register_permission_widget("action.tech_pack.edit_style", save_button)
    save_button.configure(state="normal" if self.is_action_allowed("action.tech_pack.edit_style") else "disabled")

    dialog.bind("<Return>", lambda _event: save_style())
    self.center_dialog(dialog, min_width=420, min_height=300)
    style_entry.focus_set()


def open_edit_selected_tech_pack_season_dialog(self) -> None:
    if not self.is_action_allowed("action.tech_pack.edit_style"):
        return
    selected_styles = sorted(
        normalize_style_identifier(style_code)
        for style_code in set(getattr(self, "checked_tech_pack_styles", set()) or set())
        if normalize_style_identifier(style_code)
    )
    if not selected_styles:
        messagebox.showwarning("Edit season", "Please tick at least one Tech Pack style.")
        return

    rows_by_style = {
        normalize_style_identifier(row.get("style_code")): dict(row)
        for row in list(getattr(self, "tech_pack_rows", []) or [])
        if normalize_style_identifier(row.get("style_code"))
    }
    library_by_style = {
        normalize_style_identifier(row.get("style_code")): dict(row)
        for row in list(getattr(self, "tech_pack_library", []) or [])
        if normalize_style_identifier(row.get("style_code"))
    }

    def current_season_parts(style_code: str) -> tuple[str, str]:
        row = rows_by_style.get(style_code, {})
        record = library_by_style.get(style_code, {})
        overview_data = record.get("overview_data", {})
        if not isinstance(overview_data, dict):
            overview_data = {}
        season_source = (
            overview_data.get("season")
            or row.get("season_base")
            or record.get("season")
            or row.get("season")
        )
        year_source = overview_data.get("season_year") or record.get("season_year") or row.get("season_year")
        if hasattr(self, "split_season_display"):
            return self.split_season_display(season_source, year_source)
        return str(season_source or "").strip(), str(year_source or "").strip()

    season_pairs = [current_season_parts(style_code) for style_code in selected_styles]
    common_season = season_pairs[0][0] if season_pairs and all(pair[0] == season_pairs[0][0] for pair in season_pairs) else ""
    common_year = season_pairs[0][1] if season_pairs and all(pair[1] == season_pairs[0][1] for pair in season_pairs) else ""

    dialog = tk.Toplevel(self.root)
    dialog.title("Edit season")
    dialog.transient(self.root)
    dialog.grab_set()
    dialog.minsize(420, 270)

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

    ttk.Label(frame, text="Edit season", style="Section.TLabel").grid(row=0, column=0, sticky="w")
    ttk.Label(frame, text=f"{len(selected_styles)} selected style(s)").grid(row=1, column=0, sticky="w", pady=(8, 12))

    ttk.Label(frame, text="Season").grid(row=2, column=0, sticky="w", pady=(0, 6))
    season_options = self.collect_existing_tech_pack_season_options()
    if common_season and common_season not in season_options:
        season_options = sorted([*season_options, common_season], key=lambda item: item.upper())
    season_var = tk.StringVar(value=common_season or (season_options[0] if season_options else ""))
    season_combo = ttk.Combobox(frame, textvariable=season_var, values=season_options, state="readonly")
    season_combo.grid(row=3, column=0, sticky="ew")

    ttk.Label(frame, text="Season Year").grid(row=4, column=0, sticky="w", pady=(12, 6))
    season_year_var = tk.StringVar(value=common_year)
    season_year_entry = ttk.Entry(frame, textvariable=season_year_var)
    season_year_entry.grid(row=5, column=0, sticky="ew")

    status_var = tk.StringVar(value="")
    ttk.Label(frame, textvariable=status_var, foreground="#8a4b1f").grid(row=6, column=0, sticky="w", pady=(10, 0))

    def save_selected_season() -> None:
        raw_season = str(season_var.get() or "").strip()
        if not raw_season:
            status_var.set("Please choose a season.")
            season_combo.focus_set()
            return
        if hasattr(self, "split_season_display"):
            season_base, season_year = self.split_season_display(raw_season, season_year_var.get())
        else:
            season_base = raw_season
            season_year = str(season_year_var.get() or "").strip()
        if hasattr(self, "normalize_season_year"):
            season_year = self.normalize_season_year(season_year)
        season_display = self.build_season_display(season_base, season_year) if hasattr(self, "build_season_display") else season_base
        style_updates: dict[str, dict] = {}
        for style_code in selected_styles:
            record = dict(library_by_style.get(style_code, self.default_tech_pack_record(style_code)))
            overview_data = record.get("overview_data", {})
            if not isinstance(overview_data, dict):
                overview_data = {}
            overview_data = dict(overview_data)
            overview_data["season"] = season_base
            overview_data["season_year"] = season_year
            changes = {
                "season": season_base,
                "season_year": season_year,
                "season_display": season_display,
                "style_identity": self.build_style_identity(style_code, season_base, season_year) if hasattr(self, "build_style_identity") else style_code,
                "overview_data": overview_data,
            }
            saved_record = self.save_tech_pack_record_patch_to_source(style_code, changes)
            if isinstance(saved_record, dict):
                style_updates[style_code] = saved_record
        if style_updates:
            merged_library = dict(library_by_style)
            merged_library.update(style_updates)
            self.tech_pack_library = list(merged_library.values())
        if hasattr(self, "invalidate_style_season_lookup_cache"):
            self.invalidate_style_season_lookup_cache()
        self.tech_pack_rows = self.build_tech_pack_rows()
        self.apply_tech_pack_filter()
        if hasattr(self, "sync_costing_item_seasons_from_tech_pack") and self.sync_costing_item_seasons_from_tech_pack() and hasattr(self, "costing_tree"):
            self.apply_costing_filter()
        if hasattr(self, "refresh_samples_module"):
            self.refresh_samples_module()
        if hasattr(self, "refresh_production_module"):
            self.refresh_production_module()
        if hasattr(self, "save_main_ui_session_cache"):
            self.save_main_ui_session_cache(
                "tech_pack",
                {
                    "costing_data": getattr(self, "costing_data", None),
                    "costing_items": list(getattr(self, "costing_items", []) or []),
                    "tech_pack_library": list(getattr(self, "tech_pack_library", []) or []),
                    "tech_pack_hidden_styles": sorted(getattr(self, "tech_pack_hidden_styles", set()) or []),
                    "tech_pack_rows": list(getattr(self, "tech_pack_rows", []) or []),
                },
            )
        self.set_status(f"Updated season for {len(style_updates)} Tech Pack style(s).")
        dialog.destroy()

    actions = ttk.Frame(frame)
    actions.grid(row=7, column=0, sticky="e", pady=(16, 0))
    ttk.Button(actions, text=self.t("close"), command=dialog.destroy).pack(side="left", padx=(0, 8))
    save_button = ttk.Button(actions, text=self.t("collection_list_save"), command=save_selected_season)
    save_button.pack(side="left")
    self.register_permission_widget("action.tech_pack.edit_style", save_button)
    save_button.configure(state="normal" if self.is_action_allowed("action.tech_pack.edit_style") else "disabled")

    dialog.bind("<Return>", lambda _event: save_selected_season())
    self.center_dialog(dialog, min_width=420, min_height=270)
    season_combo.focus_set()


def build_tech_pack_image_signature(self, image: Image.Image, size: int = 64) -> dict[str, object] | None:
    try:
        rgb_image = image.convert("RGB")
        grayscale_source = rgb_image.convert("L")

        # Trim large white margins so sketches are compared by silhouette, not page whitespace.
        inverted = ImageOps.invert(grayscale_source)
        bbox = inverted.point(lambda value: 255 if value > 18 else 0).getbbox()
        cropped_rgb = rgb_image.crop(bbox) if bbox else rgb_image

        contained = Image.new("RGB", (size, size), "#ffffff")
        preview = cropped_rgb.copy()
        preview.thumbnail((size, size), Image.Resampling.LANCZOS)
        offset_x = (size - preview.width) // 2
        offset_y = (size - preview.height) // 2
        contained.paste(preview, (offset_x, offset_y))

        grayscale = contained.convert("L")
        autocontrast = ImageOps.autocontrast(grayscale)
        edge_map = autocontrast.filter(ImageFilter.FIND_EDGES)
        edge_map = edge_map.filter(ImageFilter.MaxFilter(3))
        binary_mask = autocontrast.point(lambda value: 255 if value < 235 else 0)

        rgb_stats = ImageStat.Stat(contained)
        gray_stats = ImageStat.Stat(autocontrast)
        edge_stats = ImageStat.Stat(edge_map)
        mask_stats = ImageStat.Stat(binary_mask)

        horizontal_profile = []
        vertical_profile = []
        for y_index in range(size):
            row_sum = 0
            for x_index in range(size):
                row_sum += 1 if binary_mask.getpixel((x_index, y_index)) > 0 else 0
            horizontal_profile.append(row_sum / size)
        for x_index in range(size):
            col_sum = 0
            for y_index in range(size):
                col_sum += 1 if binary_mask.getpixel((x_index, y_index)) > 0 else 0
            vertical_profile.append(col_sum / size)

        content_bbox = binary_mask.getbbox()
        bbox_ratio = 0.0
        bbox_aspect = 0.0
        if content_bbox:
            left, top, right, bottom = content_bbox
            bbox_width = max(1, right - left)
            bbox_height = max(1, bottom - top)
            bbox_ratio = (bbox_width * bbox_height) / float(size * size)
            bbox_aspect = bbox_width / float(bbox_height)

        return {
            "rgb": tuple(tuple(int(channel) for channel in pixel) for pixel in contained.getdata()),
            "gray": tuple(int(pixel) for pixel in autocontrast.getdata()),
            "edge": tuple(int(pixel) for pixel in edge_map.getdata()),
            "mask": tuple(1 if pixel > 0 else 0 for pixel in binary_mask.getdata()),
            "mean_rgb": tuple(float(value) for value in rgb_stats.mean),
            "mean_gray": float(gray_stats.mean[0]),
            "mean_edge": float(edge_stats.mean[0]),
            "mask_density": float(mask_stats.mean[0]) / 255.0,
            "horizontal_profile": tuple(horizontal_profile),
            "vertical_profile": tuple(vertical_profile),
            "bbox_ratio": float(bbox_ratio),
            "bbox_aspect": float(bbox_aspect),
        }
    except Exception:
        return None


def get_cached_tech_pack_cover_signature(self, image_path: str) -> dict[str, object] | None:
    cache_key = str(image_path or "").strip()
    if not cache_key:
        return None
    cache = getattr(self, "tech_pack_cover_signature_cache", {})
    if cache_key in cache:
        return cache[cache_key]
    resolved = resolve_image_source(cache_key)
    if resolved is None:
        cache[cache_key] = None
        self.tech_pack_cover_signature_cache = cache
        return None
    try:
        with Image.open(resolved) as image:
            signature = self.build_tech_pack_image_signature(image)
    except Exception:
        signature = None
    cache[cache_key] = signature
    self.tech_pack_cover_signature_cache = cache
    return signature


def calculate_tech_pack_image_similarity(
    self,
    source_signature: dict[str, object] | None,
    target_signature: dict[str, object] | None,
) -> float:
    if not source_signature or not target_signature:
        return 0.0

    source_rgb = source_signature.get("rgb")
    target_rgb = target_signature.get("rgb")
    source_gray = source_signature.get("gray")
    target_gray = target_signature.get("gray")
    source_edge = source_signature.get("edge")
    target_edge = target_signature.get("edge")
    source_mask = source_signature.get("mask")
    target_mask = target_signature.get("mask")
    source_mean_rgb = source_signature.get("mean_rgb")
    target_mean_rgb = target_signature.get("mean_rgb")
    source_mean_gray = source_signature.get("mean_gray")
    target_mean_gray = target_signature.get("mean_gray")
    source_mean_edge = source_signature.get("mean_edge")
    target_mean_edge = target_signature.get("mean_edge")
    source_mask_density = source_signature.get("mask_density")
    target_mask_density = target_signature.get("mask_density")
    source_horizontal_profile = source_signature.get("horizontal_profile")
    target_horizontal_profile = target_signature.get("horizontal_profile")
    source_vertical_profile = source_signature.get("vertical_profile")
    target_vertical_profile = target_signature.get("vertical_profile")
    source_bbox_ratio = source_signature.get("bbox_ratio")
    target_bbox_ratio = target_signature.get("bbox_ratio")
    source_bbox_aspect = source_signature.get("bbox_aspect")
    target_bbox_aspect = target_signature.get("bbox_aspect")

    if (
        not isinstance(source_rgb, tuple)
        or not isinstance(target_rgb, tuple)
        or not isinstance(source_gray, tuple)
        or not isinstance(target_gray, tuple)
        or not isinstance(source_edge, tuple)
        or not isinstance(target_edge, tuple)
        or not isinstance(source_mask, tuple)
        or not isinstance(target_mask, tuple)
        or len(source_rgb) != len(target_rgb)
        or len(source_gray) != len(target_gray)
        or len(source_edge) != len(target_edge)
        or len(source_mask) != len(target_mask)
    ):
        return 0.0

    rgb_difference = sum(
        sum(abs(current_channel - target_channel) for current_channel, target_channel in zip(current, target))
        for current, target in zip(source_rgb, target_rgb)
        if isinstance(current, tuple) and isinstance(target, tuple)
    ) / (len(source_rgb) * 3)
    gray_difference = sum(abs(current - target) for current, target in zip(source_gray, target_gray)) / len(source_gray)
    edge_difference = sum(abs(current - target) for current, target in zip(source_edge, target_edge)) / len(source_edge)
    mask_difference = sum(abs(current - target) for current, target in zip(source_mask, target_mask)) / len(source_mask)

    intersection = sum(1 for current, target in zip(source_mask, target_mask) if current and target)
    union = sum(1 for current, target in zip(source_mask, target_mask) if current or target)
    mask_iou = intersection / union if union else 0.0

    mean_rgb_difference = 0.0
    if isinstance(source_mean_rgb, tuple) and isinstance(target_mean_rgb, tuple):
        mean_rgb_difference = sum(
            abs(float(current) - float(target))
            for current, target in zip(source_mean_rgb, target_mean_rgb)
        ) / max(1, len(source_mean_rgb))
    mean_gray_difference = abs(float(source_mean_gray or 0.0) - float(target_mean_gray or 0.0))
    mean_edge_difference = abs(float(source_mean_edge or 0.0) - float(target_mean_edge or 0.0))
    density_difference = abs(float(source_mask_density or 0.0) - float(target_mask_density or 0.0))
    bbox_ratio_difference = abs(float(source_bbox_ratio or 0.0) - float(target_bbox_ratio or 0.0))
    bbox_aspect_difference = abs(float(source_bbox_aspect or 0.0) - float(target_bbox_aspect or 0.0))

    horizontal_profile_difference = 1.0
    if isinstance(source_horizontal_profile, tuple) and isinstance(target_horizontal_profile, tuple):
        horizontal_profile_difference = sum(
            abs(float(current) - float(target))
            for current, target in zip(source_horizontal_profile, target_horizontal_profile)
        ) / max(1, len(source_horizontal_profile))
    vertical_profile_difference = 1.0
    if isinstance(source_vertical_profile, tuple) and isinstance(target_vertical_profile, tuple):
        vertical_profile_difference = sum(
            abs(float(current) - float(target))
            for current, target in zip(source_vertical_profile, target_vertical_profile)
        ) / max(1, len(source_vertical_profile))

    weighted_difference = (
        rgb_difference * 0.08
        + gray_difference * 0.07
        + edge_difference * 0.20
        + mask_difference * 70.0 * 0.22
        + mean_rgb_difference * 0.04
        + mean_gray_difference * 0.02
        + mean_edge_difference * 0.02
        + density_difference * 255.0 * 0.10
        + bbox_ratio_difference * 255.0 * 0.08
        + bbox_aspect_difference * 80.0 * 0.07
        + horizontal_profile_difference * 255.0 * 0.05
        + vertical_profile_difference * 255.0 * 0.05
    )
    similarity = max(0.0, 100.0 - (weighted_difference / 255.0 * 100.0))
    similarity *= 0.35 + (mask_iou * 0.65)
    if edge_difference > 32:
        similarity *= 0.72
    if mask_difference > 0.18:
        similarity *= 0.62
    if density_difference > 0.12:
        similarity *= 0.78
    if bbox_aspect_difference > 0.55:
        similarity *= 0.70
    return round(max(0.0, min(100.0, similarity)), 1)


def focus_tech_pack_style_in_list(self, style_code: str, *, open_detail: bool = False) -> bool:
    target_style = str(style_code or "").strip().lower()
    if not target_style:
        return False
    self.switch_module("tech_pack")
    for row in getattr(self, "tech_pack_rows", []):
        if str(row.get("style_code", "")).strip().lower() == target_style:
            self._tech_pack_filter_update_suspended = True
            try:
                self.tech_pack_filter_field_internal = "__all__"
                if hasattr(self, "translate_tech_pack_filter_field"):
                    self.tech_pack_filter_field_var.set(self.translate_tech_pack_filter_field("__all__"))
                self.tech_pack_date_from_var.set("")
                self.tech_pack_date_to_var.set("")
            finally:
                self._tech_pack_filter_update_suspended = False
            self.update_tech_pack_filter_input_state()
            self.sync_tech_pack_search_query(str(row.get("style_code", "")).strip())
            break
    self.apply_tech_pack_filter()
    for index, row in enumerate(self.filtered_tech_pack_rows, start=1):
        if str(row.get("style_code", "")).strip().lower() != target_style:
            continue
        row_id = str(index)
        self.tech_pack_tree.selection_set(row_id)
        self.tech_pack_tree.focus(row_id)
        self.tech_pack_tree.see(row_id)
        self.tech_pack_selected_index = index - 1
        self.on_tech_pack_item_selected(None)
        if open_detail:
            self.open_tech_pack_detail_window(row)
        return True
    return False


def show_tech_pack_image_search_results(self, query_image: Image.Image, results: list[dict]) -> None:
    dialog = tk.Toplevel(self.root)
    dialog.title(self.get_tech_pack_image_search_text("title"))
    dialog.resizable(True, True)
    dialog.grab_set()
    dialog.minsize(860, 520)

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

    preview_frame = ttk.LabelFrame(container, text=self.get_tech_pack_image_search_text("button"))
    preview_frame.grid(row=0, column=0, rowspan=2, sticky="nsw", padx=(0, 14))
    query_preview = query_image.copy()
    query_preview.thumbnail((180, 180), Image.Resampling.LANCZOS)
    preview_photo = ImageTk.PhotoImage(query_preview)
    preview_label = ttk.Label(preview_frame, image=preview_photo)
    preview_label.image = preview_photo
    preview_label.pack(padx=10, pady=10)

    result_title = ttk.Label(container, text=self.get_tech_pack_image_search_text("results"), style="Section.TLabel")
    result_title.grid(row=0, column=1, sticky="w")

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

    style = ttk.Style(dialog)
    search_tree_style = "TechPackSearch.Treeview"
    style.configure(search_tree_style, rowheight=78)
    columns = ("similarity", "buyer", "style_code", "season", "style_name")
    result_tree = ttk.Treeview(
        result_frame,
        columns=columns,
        show="tree headings",
        selectmode="browse",
        height=10,
        style=search_tree_style,
    )
    result_tree.heading("#0", text=self.t("tech_pack_image_column"))
    result_tree.heading("similarity", text=self.get_tech_pack_image_search_text("similarity"))
    result_tree.heading("buyer", text=self.t("buyer_name"))
    result_tree.heading("style_code", text=self.t("tech_pack_style_column"))
    result_tree.heading("season", text=self.t("tech_pack_season_column"))
    result_tree.heading("style_name", text=self.get_tech_pack_image_search_text("style_name"))
    result_tree.column("#0", width=96, anchor="center", stretch=False)
    result_tree.column("similarity", width=90, anchor="center", stretch=False)
    result_tree.column("buyer", width=90, anchor="w", stretch=False)
    result_tree.column("style_code", width=110, anchor="w", stretch=False)
    result_tree.column("season", width=110, anchor="center", stretch=False)
    result_tree.column("style_name", width=260, anchor="w")
    result_tree_photos: list[ImageTk.PhotoImage] = []
    for index, row in enumerate(results, start=1):
        thumbnail_photo = self.create_tech_pack_thumbnail(row, target_width=76, target_height=76)
        if thumbnail_photo is not None:
            result_tree_photos.append(thumbnail_photo)
        insert_kwargs = {
            "text": "",
            "values": (
                f"{float(row.get('similarity', 0.0)):.1f}",
                self.display_value(row.get("buyer")),
                self.display_value(row.get("style_code")),
                self.display_value(row.get("season")),
                self.display_value(row.get("style_name")),
            ),
        }
        if thumbnail_photo is not None:
            insert_kwargs["image"] = thumbnail_photo
        result_tree.insert(
            "",
            "end",
            str(index),
            **insert_kwargs,
        )
    result_tree.grid(row=0, column=0, sticky="nsew")
    result_tree._preview_images = result_tree_photos  # type: ignore[attr-defined]
    result_scroll = ttk.Scrollbar(result_frame, orient="vertical", command=result_tree.yview)
    result_scroll.grid(row=0, column=1, sticky="ns")
    result_tree.configure(yscrollcommand=result_scroll.set)

    def open_selected(*_args: object) -> None:
        selection = result_tree.selection()
        if not selection:
            return
        row = results[int(selection[0]) - 1]
        dialog.destroy()
        self.focus_tech_pack_style_in_list(str(row.get("style_code", "")).strip(), open_detail=False)

    def open_detail_selected(*_args: object) -> None:
        selection = result_tree.selection()
        if not selection:
            return
        row = results[int(selection[0]) - 1]
        dialog.destroy()
        self.focus_tech_pack_style_in_list(str(row.get("style_code", "")).strip(), open_detail=True)

    actions = ttk.Frame(container)
    actions.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(14, 0))
    ttk.Button(actions, text=self.t("close"), command=dialog.destroy).pack(side="right")
    ttk.Button(actions, text=self.get_tech_pack_image_search_text("open"), command=open_selected).pack(
        side="right",
        padx=(0, 8),
    )
    result_tree.bind("<Double-1>", open_detail_selected)
    if results:
        result_tree.selection_set("1")
        result_tree.focus("1")

    self.center_dialog(dialog, min_width=860, min_height=520)


def load_tech_pack_image_from_clipboard(self) -> Image.Image | None:
    clipboard_data = ImageGrab.grabclipboard()
    if hasattr(clipboard_data, "convert"):
        try:
            return clipboard_data.convert("RGB")
        except Exception:
            return None

    image_suffixes = {".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp", ".tif", ".tiff"}

    if isinstance(clipboard_data, list):
        for item in clipboard_data:
            candidate = Path(str(item).strip().strip('"'))
            if not str(candidate):
                continue
            if candidate.suffix.lower() not in image_suffixes:
                continue
            try:
                with Image.open(candidate) as image_file:
                    return image_file.convert("RGB")
            except Exception:
                continue

    clipboard_text = ""
    try:
        clipboard_text = str(self.root.clipboard_get() or "").strip()
    except Exception:
        clipboard_text = ""
    if clipboard_text:
        normalized = clipboard_text.strip().strip('"')
        candidate_paths = [Path(normalized)]
        if "\n" in normalized:
            candidate_paths.extend(Path(part.strip().strip('"')) for part in normalized.splitlines() if part.strip())
        for candidate in candidate_paths:
            if candidate.suffix.lower() not in image_suffixes:
                continue
            if not candidate.exists():
                continue
            try:
                with Image.open(candidate) as image_file:
                    return image_file.convert("RGB")
            except Exception:
                continue
    return None


def load_tech_pack_image_from_path(self, raw_path: str) -> Image.Image | None:
    candidate = Path(str(raw_path or "").strip().strip('"'))
    if not str(candidate) or not candidate.exists() or not candidate.is_file():
        return None
    if candidate.suffix.lower() not in {".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp", ".tif", ".tiff"}:
        return None
    try:
        with Image.open(candidate) as image_file:
            return image_file.convert("RGB")
    except Exception:
        return None


def run_tech_pack_image_search(self, query_image: Image.Image) -> None:
    query_signature = self.build_tech_pack_image_signature(query_image)
    if not query_signature:
        messagebox.showinfo(
            self.get_tech_pack_image_search_text("title"),
            self.get_tech_pack_image_search_text("image_invalid"),
        )
        return

    if not getattr(self, "tech_pack_rows", []):
        self.refresh_tech_pack_module(silent=True)
    searchable_rows = [row for row in self.tech_pack_rows if str(row.get("image_path", "")).strip()]
    if not searchable_rows:
        messagebox.showinfo(
            self.get_tech_pack_image_search_text("title"),
            self.get_tech_pack_image_search_text("no_library"),
        )
        return

    self.set_status(self.get_tech_pack_image_search_text("progress"))
    self.root.config(cursor="watch")
    self.root.update_idletasks()
    try:
        matches: list[dict] = []
        for row in searchable_rows:
            signature = self.get_cached_tech_pack_cover_signature(str(row.get("image_path", "")).strip())
            similarity = self.calculate_tech_pack_image_similarity(query_signature, signature)
            if similarity <= 0:
                continue
            result_row = dict(row)
            result_row["similarity"] = similarity
            matches.append(result_row)
    finally:
        self.root.config(cursor="")

    matches.sort(
        key=lambda row: (
            float(row.get("similarity", 0.0)),
            str(row.get("style_code", "")).strip().lower(),
        ),
        reverse=True,
    )
    strong_matches = [row for row in matches if float(row.get("similarity", 0.0)) >= 55.0]
    final_results = (strong_matches or matches[:10])[:25]
    if not final_results:
        messagebox.showinfo(
            self.get_tech_pack_image_search_text("title"),
            self.get_tech_pack_image_search_text("no_match"),
        )
        self.set_status(self.get_tech_pack_image_search_text("no_match"))
        return
    self.set_status(self.get_tech_pack_image_search_text("status", count=len(final_results)))
    self.show_tech_pack_image_search_results(query_image, final_results)


def open_tech_pack_image_search_dialog(self) -> None:
    dialog = tk.Toplevel(self.root)
    dialog.title(self.get_tech_pack_image_search_text("title"))
    dialog.resizable(True, True)
    dialog.grab_set()
    dialog.minsize(760, 520)

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

    ttk.Label(frame, text=self.get_tech_pack_image_search_text("input_title"), style="Section.TLabel").grid(
        row=0,
        column=0,
        sticky="w",
    )
    ttk.Label(frame, text=self.get_tech_pack_image_search_text("input_hint")).grid(
        row=1,
        column=0,
        sticky="w",
        pady=(6, 12),
    )

    selected_image: dict[str, Image.Image | None] = {"image": None}
    selected_name = tk.StringVar(value=self.get_tech_pack_image_search_text("source_missing"))
    path_var = tk.StringVar(value="")

    input_row = ttk.Frame(frame)
    input_row.grid(row=2, column=0, sticky="ew")
    input_row.columnconfigure(1, weight=1)

    preview_panel = ttk.LabelFrame(input_row, text=self.get_tech_pack_image_search_text("button"))
    preview_panel.grid(row=0, column=0, sticky="nsw", padx=(0, 14))
    preview_label = ttk.Label(preview_panel, anchor="center")
    preview_label.pack(padx=10, pady=10)

    controls = ttk.Frame(input_row)
    controls.grid(row=0, column=1, sticky="nsew")
    controls.columnconfigure(0, weight=1)

    button_row = ttk.Frame(controls)
    button_row.grid(row=0, column=0, sticky="w")

    drop_frame = ttk.LabelFrame(controls, text=self.get_tech_pack_image_search_text("drop_here"))
    drop_frame.grid(row=1, column=0, sticky="nsew", pady=(12, 0))
    drop_frame.columnconfigure(0, weight=1)

    drop_label = ttk.Label(
        drop_frame,
        text=self.get_tech_pack_image_search_text("drop_hint"),
        anchor="center",
        justify="center",
        padding=18,
    )
    drop_label.grid(row=0, column=0, sticky="nsew")

    path_row = ttk.Frame(controls)
    path_row.grid(row=2, column=0, sticky="ew", pady=(12, 0))
    path_row.columnconfigure(0, weight=1)
    path_entry = ttk.Entry(path_row, textvariable=path_var)
    path_entry.grid(row=0, column=0, sticky="ew")

    status_label = ttk.Label(controls, textvariable=selected_name, wraplength=420, anchor="w", justify="left")
    status_label.grid(row=3, column=0, sticky="ew", pady=(12, 0))

    def update_preview(image: Image.Image, source_name: str) -> None:
        selected_image["image"] = image.copy()
        preview = image.copy()
        preview.thumbnail((180, 180), Image.Resampling.LANCZOS)
        photo = ImageTk.PhotoImage(preview)
        preview_label.configure(image=photo)
        preview_label.image = photo
        selected_name.set(self.get_tech_pack_image_search_text("source_ready", name=source_name))

    def load_from_clipboard(*_args: object) -> None:
        image = self.load_tech_pack_image_from_clipboard()
        if image is None:
            messagebox.showinfo(
                self.get_tech_pack_image_search_text("title"),
                self.get_tech_pack_image_search_text("clipboard_invalid"),
            )
            return
        update_preview(image, "Clipboard")

    def load_from_file_dialog() -> None:
        path = filedialog.askopenfilename(
            title=self.get_tech_pack_image_search_text("title"),
            filetypes=[
                ("Image files", "*.png;*.jpg;*.jpeg;*.bmp;*.gif;*.webp;*.tif;*.tiff"),
                ("All files", "*.*"),
            ],
        )
        if not path:
            return
        path_var.set(path)
        image = self.load_tech_pack_image_from_path(path)
        if image is None:
            messagebox.showinfo(
                self.get_tech_pack_image_search_text("title"),
                self.get_tech_pack_image_search_text("path_invalid"),
            )
            return
        update_preview(image, Path(path).name)

    def load_from_path_entry() -> None:
        path = path_var.get().strip()
        image = self.load_tech_pack_image_from_path(path)
        if image is None:
            messagebox.showinfo(
                self.get_tech_pack_image_search_text("title"),
                self.get_tech_pack_image_search_text("path_invalid"),
            )
            return
        update_preview(image, Path(path).name)

    def perform_search() -> None:
        image = selected_image.get("image")
        if image is None:
            messagebox.showinfo(
                self.get_tech_pack_image_search_text("title"),
                self.get_tech_pack_image_search_text("source_missing"),
            )
            return
        dialog.destroy()
        self.run_tech_pack_image_search(image)

    ttk.Button(button_row, text=self.get_tech_pack_image_search_text("paste"), command=load_from_clipboard).pack(
        side="left"
    )
    ttk.Button(button_row, text=self.get_tech_pack_image_search_text("browse"), command=load_from_file_dialog).pack(
        side="left",
        padx=(8, 0),
    )
    ttk.Button(path_row, text=self.get_tech_pack_image_search_text("path"), command=load_from_path_entry).grid(
        row=0,
        column=1,
        padx=(8, 0),
    )

    path_entry.insert(0, self.get_tech_pack_image_search_text("path_placeholder"))

    def clear_placeholder(_event: object) -> None:
        if path_var.get() == self.get_tech_pack_image_search_text("path_placeholder"):
            path_var.set("")

    path_entry.bind("<FocusIn>", clear_placeholder)
    dialog.bind("<Control-v>", load_from_clipboard)

    if windnd is not None:
        def handle_drop(files: list[bytes]) -> None:
            for item in files:
                try:
                    decoded = item.decode("utf-8")
                except Exception:
                    decoded = item.decode("mbcs", errors="ignore")
                image = self.load_tech_pack_image_from_path(decoded)
                if image is None:
                    continue
                path_var.set(decoded)
                update_preview(image, Path(decoded).name)
                return
            messagebox.showinfo(
                self.get_tech_pack_image_search_text("title"),
                self.get_tech_pack_image_search_text("path_invalid"),
            )

        windnd.hook_dropfiles(drop_label, func=handle_drop)
        windnd.hook_dropfiles(drop_frame, func=handle_drop)
    else:
        drop_label.configure(text=self.get_tech_pack_image_search_text("drop_hint") + "\nDrag and drop is unavailable on this machine.")

    actions = ttk.Frame(frame)
    actions.grid(row=4, column=0, sticky="ew", pady=(14, 0))
    ttk.Button(actions, text=self.t("close"), command=dialog.destroy).pack(side="right")
    ttk.Button(actions, text=self.get_tech_pack_image_search_text("search_now"), command=perform_search).pack(
        side="right",
        padx=(0, 8),
    )

    self.center_dialog(dialog, min_width=760, min_height=520)


def search_tech_pack_by_image(self) -> None:
    self.open_tech_pack_image_search_dialog()


def set_sync_tech_pack_running(self, is_running: bool) -> None:
    self.sync_tech_pack_running = bool(is_running)
    button_text = self.t("tech_pack_sync_button_running") if is_running else self.t("tech_pack_sync_button")
    for widget_key in ("tech_pack_sync_button", "samples_sync_tech_pack_button"):
        if widget_key not in self.widget_text:
            continue
        try:
            self.widget_text[widget_key].configure(
                text=button_text,
                state="disabled" if is_running else "normal",
            )
        except Exception:
            pass
    if not is_running and hasattr(self, "apply_permission_state"):
        try:
            self.apply_permission_state()
        except Exception:
            pass


def sync_tech_pack_styles(self, selected_styles: list[str]) -> None:
    if getattr(self, "sync_tech_pack_running", False):
        messagebox.showinfo(self.t("app_title"), self.t("tech_pack_sync_running"))
        return
    if not self.is_action_allowed("action.tool.download_tech_pack"):
        return
    if not self.is_action_allowed("action.tech_pack.import_pdf"):
        messagebox.showwarning(self.t("warning_title"), self.t("tech_pack_sync_import_permission"))
        return
    normalized_styles = sorted(
        {
            normalize_style_identifier(style_code)
            for style_code in selected_styles
            if normalize_style_identifier(style_code)
        }
    )
    if not normalized_styles:
        messagebox.showwarning(self.t("warning_title"), self.t("tech_pack_sync_need_styles"))
        return
    user = self.download_tech_pack_user_var.get().strip()
    password = self.download_tech_pack_password_var.get()
    if not user:
        messagebox.showwarning(self.t("download_tech_pack_missing_title"), self.t("download_tech_pack_missing_user"))
        return
    if not password:
        messagebox.showwarning(self.t("download_tech_pack_missing_title"), self.t("download_tech_pack_missing_password"))
        return
    download_dir = self.download_tech_pack_download_folder_var.get().strip() or "D:\\download\\tech pack"
    self.persist_download_tech_pack_settings()
    self.set_sync_tech_pack_running(True)
    self.set_status(self.t("tech_pack_sync_starting", count=len(normalized_styles)))
    worker = threading.Thread(
        target=self._run_sync_tech_pack_worker,
        kwargs={
            "user": user,
            "password": password,
            "styles": normalized_styles,
            "download_dir": download_dir,
            "headless": bool(self.download_tech_pack_headless_var.get()),
            "debug_mode": bool(self.download_tech_pack_debug_var.get()),
            "include_pattern_zip": bool(self.download_tech_pack_pattern_zip_var.get()),
            "include_comment_report": bool(self.download_tech_pack_comment_var.get()),
        },
        daemon=True,
        name="sync-tech-pack",
    )
    worker.start()


def get_tech_pack_sync_search_style_code(self, style_code: object) -> str:
    normalized_style = normalize_style_identifier(style_code)
    if re.fullmatch(r"[A-Z]+[0-9]+P", normalized_style):
        return normalized_style[:-1]
    return normalized_style


def parse_sync_tech_pack_report_paths(self, result_text: str, selected_styles: list[str]) -> list[Path]:
    style_set = {str(style).strip().upper() for style in selected_styles if str(style).strip()}
    resolved_paths: list[Path] = []
    seen_paths: set[str] = set()
    for raw_line in str(result_text or "").splitlines():
        line = raw_line.strip()
        if not line or "->" not in line or ":" not in line:
            continue
        style_prefix, payload = line.split(":", 1)
        style_code = style_prefix.strip().upper()
        if style_set and style_code not in style_set:
            continue
        _, downloaded_items = payload.split("->", 1)
        for chunk in downloaded_items.split("|"):
            candidate = chunk.strip()
            if not candidate.lower().endswith(".pdf"):
                continue
            candidate_path = Path(candidate)
            path_key = str(candidate_path).strip().lower()
            if not candidate_path.exists() or path_key in seen_paths:
                continue
            seen_paths.add(path_key)
            resolved_paths.append(candidate_path)
    return resolved_paths


def reconcile_synced_tech_pack_style_alias(
    self,
    target_style_code: object,
    search_style_code: object,
    *,
    library_rows: list[dict] | None = None,
    hidden_styles: set[str] | None = None,
) -> tuple[list[dict], set[str]]:
    target_style = normalize_style_identifier(target_style_code)
    search_style = normalize_style_identifier(search_style_code)
    current_library = [
        dict(item)
        for item in (
            library_rows
            if library_rows is not None
            else self.load_tech_pack_library_from_source()
        )
        if isinstance(item, dict)
    ]
    current_hidden = set(
        hidden_styles
        if hidden_styles is not None
        else self.load_tech_pack_hidden_styles_from_source()
    )
    if not target_style or not search_style or target_style == search_style:
        return current_library, current_hidden

    library_by_style = {
        normalize_style_identifier(item.get("style_code")): dict(item)
        for item in current_library
        if normalize_style_identifier(item.get("style_code"))
    }
    source_record = dict(library_by_style.get(search_style, {}))
    if not source_record:
        return current_library, current_hidden

    target_record = dict(source_record)
    target_record["style_code"] = target_style
    target_record["source_style_code"] = str(source_record.get("source_style_code") or search_style).strip()
    target_record["customer_search_style_code"] = str(
        source_record.get("customer_search_style_code") or search_style
    ).strip()

    existing_target = dict(library_by_style.get(target_style, {}))
    if existing_target:
        created_at = str(existing_target.get("created_at", "")).strip()
        if created_at and not str(target_record.get("created_at", "")).strip():
            target_record["created_at"] = created_at

    saved_target = self.save_tech_pack_record_to_source(
        target_record,
        upload_images=False,
        queue_image_sync=False,
    )
    library_by_style[target_style] = dict(saved_target or target_record)
    current_hidden.discard(target_style)
    saved_hidden = set(self.save_tech_pack_hidden_styles_to_source(current_hidden))
    saved_library = [
        dict(item)
        for item in self.load_tech_pack_library_from_source()
        if isinstance(item, dict)
    ]
    return saved_library, saved_hidden


def _run_sync_tech_pack_worker(
    self,
    *,
    user: str,
    password: str,
    styles: list[str],
    download_dir: str,
    headless: bool,
    debug_mode: bool,
    include_pattern_zip: bool,
    include_comment_report: bool,
) -> None:
    result_payload: dict[str, object]
    try:
        all_result_lines: list[str] = []
        failed: list[str] = []
        skipped_duplicates: list[str] = []
        imported = 0
        downloaded = 0
        queued_job_ids: list[str] = []
        queued_aliases: dict[str, dict[str, str]] = {}
        latest_library = self.load_tech_pack_library_from_source()
        latest_hidden_styles = set(self.load_tech_pack_hidden_styles_from_source())
        upload_images = self.should_use_server_data_source()
        queue_image_sync = not upload_images
        total_styles = len(styles)

        for index, style_code in enumerate(styles, start=1):
            normalized_style = str(style_code).strip()
            if not normalized_style:
                continue
            search_style = self.get_tech_pack_sync_search_style_code(normalized_style)
            try:
                status_style_text = (
                    f"{normalized_style} (search {search_style})"
                    if search_style and search_style != normalized_style
                    else normalized_style
                )
                self.schedule_root_after(
                    0,
                    lambda current=index, total=total_styles, code=status_style_text: self.set_status(
                        f"Sync Tech Pack: downloading {code} ({current}/{total})"
                    ),
                )
                result_text = run_download_tech_pack(
                    user,
                    password,
                    styles=[search_style],
                    download_dir=download_dir,
                    headless=headless,
                    debug_mode=debug_mode,
                    include_pattern_zip=include_pattern_zip,
                    include_comment_report=include_comment_report,
                    status_callback=lambda message: self.schedule_root_after(
                        0,
                        lambda text=message: self.set_status(f"Sync Tech Pack: {text}"),
                    ),
                )
                if result_text.strip():
                    all_result_lines.append(result_text.strip())
                report_paths = self.parse_sync_tech_pack_report_paths(result_text, [search_style])
                if not report_paths:
                    failed.append(f"{normalized_style}: {self.t('tech_pack_sync_no_pdf')}")
                    continue
                downloaded += len(report_paths)
                style_imported = False
                for report_path in report_paths:
                    self.schedule_root_after(
                        0,
                        lambda code=normalized_style, name=report_path.name: self.set_status(
                            f"Sync Tech Pack: importing {code} ({name})"
                        ),
                    )
                    if self.should_use_server_data_source():
                        try:
                            payload = import_tech_pack_source_file(
                                self.server_settings,
                                report_path,
                                target_style_code=normalized_style,
                            )
                            status = str(payload.get("status", "")).strip().lower()
                            if status == "done":
                                latest_library, latest_hidden_styles = self.reconcile_synced_tech_pack_style_alias(
                                    normalized_style,
                                    search_style,
                                    library_rows=None,
                                    hidden_styles=None,
                                )
                                imported += 1
                                style_imported = True
                                continue
                            if status == "error":
                                failed.append(f"{normalized_style}: {str(payload.get('error', '')).strip() or report_path.name}")
                                continue
                            job_id = str(payload.get("job_id", "")).strip()
                            if job_id:
                                queued_job_ids.append(job_id)
                                if search_style != normalized_style:
                                    queued_aliases[job_id] = {
                                        "target_style": normalized_style,
                                        "search_style": search_style,
                                    }
                                imported += 1
                                style_imported = True
                            else:
                                failed.append(report_path.name)
                        except Exception as exc:
                            failed.append(f"{normalized_style}: {str(exc).strip() or report_path.name}")
                        continue
                    result = self.import_single_tech_pack_path_sync(
                        report_path,
                        library_rows=latest_library,
                        hidden_styles=latest_hidden_styles,
                        upload_images=upload_images,
                        queue_image_sync=queue_image_sync,
                        target_style_code=normalized_style,
                    )
                    latest_library = list(result.get("library", latest_library))
                    latest_hidden_styles = set(result.get("hidden_styles", latest_hidden_styles))
                    status = str(result.get("status", "")).strip().lower()
                    if status == "imported":
                        imported += 1
                        style_imported = True
                        self.schedule_root_after(
                            0,
                            lambda rows=latest_library, hidden=set(latest_hidden_styles): (
                                setattr(self, "tech_pack_library", rows),
                                setattr(self, "tech_pack_hidden_styles", hidden),
                                self.refresh_tech_pack_module(silent=True),
                            ),
                        )
                    elif status == "duplicate":
                        skipped_duplicates.append(report_path.name)
                    else:
                        failed.append(f"{normalized_style}: {str(result.get('message', '')).strip() or report_path.name}")
                if style_imported:
                    self.schedule_root_after(
                        0,
                        lambda code=normalized_style: (
                            self.uncheck_tech_pack_style(code),
                            self.uncheck_sample_rows_for_style(code) if hasattr(self, "uncheck_sample_rows_for_style") else None,
                        ),
                    )
            except Exception as exc:
                failed.append(f"{normalized_style}: {str(exc).strip() or 'Sync failed'}")
                continue
        result_payload = {
            "status": "success",
            "result_text": "\n\n".join(all_result_lines),
            "downloaded": downloaded,
            "imported": imported,
            "failed": failed,
            "skipped_duplicates": skipped_duplicates,
            "queued_job_ids": queued_job_ids,
            "queued_aliases": queued_aliases,
            "library": latest_library,
            "hidden_styles": list(latest_hidden_styles),
            "styles": list(styles),
        }
    except Exception as exc:
        result_payload = {
            "status": "error",
            "message": str(exc),
        }
    self.schedule_root_after(0, lambda payload=result_payload: self.finish_sync_tech_pack(payload))


def finish_sync_tech_pack(self, payload: dict[str, object]) -> None:
    self.set_sync_tech_pack_running(False)
    status = str(payload.get("status", "")).strip().lower()
    if status != "success":
        message = str(payload.get("message", "")).strip() or self.t("tech_pack_sync_failed")
        messagebox.showerror(self.t("warning_title"), message)
        self.set_status(message)
        return
    self.tech_pack_library = list(payload.get("library", self.tech_pack_library))
    self.tech_pack_hidden_styles = set(payload.get("hidden_styles", self.tech_pack_hidden_styles))
    self.refresh_tech_pack_module()
    if hasattr(self, "refresh_samples_module"):
        self.refresh_samples_module()
    imported = int(payload.get("imported", 0) or 0)
    failed = [str(item).strip() for item in payload.get("failed", []) if str(item).strip()]
    skipped_duplicates = [str(item).strip() for item in payload.get("skipped_duplicates", []) if str(item).strip()]
    queued_job_ids = [str(item).strip() for item in payload.get("queued_job_ids", []) if str(item).strip()]
    if queued_job_ids:
        self.pending_tech_pack_import_job_ids.update(queued_job_ids)
        queued_aliases = payload.get("queued_aliases", {})
        if isinstance(queued_aliases, dict) and queued_aliases:
            pending_aliases = getattr(self, "pending_tech_pack_import_aliases", {})
            if not isinstance(pending_aliases, dict):
                pending_aliases = {}
            for job_id, alias_payload in queued_aliases.items():
                if isinstance(alias_payload, dict):
                    pending_aliases[str(job_id)] = dict(alias_payload)
            self.pending_tech_pack_import_aliases = pending_aliases
        self.schedule_tech_pack_import_poll(250)
    elif imported:
        self.tech_pack_library = self.load_tech_pack_library_from_source()
        self.tech_pack_hidden_styles = self.load_tech_pack_hidden_styles_from_source()
        self.refresh_tech_pack_module(silent=True)
    if failed:
        messagebox.showwarning(
            self.t("warning_title"),
            self.t("tech_pack_import_failed") + "\n\n" + "\n".join(failed[:10]),
        )
    if skipped_duplicates:
        self.show_auto_close_info(
            self.t("import_done_title"),
            "Skipped duplicate Tech Pack file(s):\n\n" + "\n".join(skipped_duplicates[:10]),
        )
    if imported:
        if queued_job_ids:
            self.show_auto_close_info(
                self.t("import_done_title"),
                f"Queued {imported} Tech Pack file(s) to the server. Images will be generated on the server in the background.",
            )
            self.set_status(f"Queued {imported} Tech Pack file(s) to the server.")
        else:
            self.show_auto_close_info(self.t("import_done_title"), self.t("tech_pack_import_done", count=imported))
            self.set_status(self.t("tech_pack_import_done", count=imported))
    elif failed:
        self.set_status(self.t("tech_pack_import_failed"))
    else:
        result_text = str(payload.get("result_text", "")).strip()
        message = self.t("tech_pack_sync_no_pdf")
        if result_text:
            message = f"{message}\n\n{result_text}"
        messagebox.showwarning(self.t("warning_title"), message)
        self.set_status(self.t("tech_pack_sync_no_pdf"))


def sync_selected_tech_pack_styles(self) -> None:
    self.sync_tech_pack_styles(sorted(self.checked_tech_pack_styles))


def bind_tech_pack_list_module(app_cls: type) -> None:
    method_map = {
        "refresh_tech_pack_module": refresh_tech_pack_module,
        "_build_tech_pack_module": _build_tech_pack_module,
        "apply_tech_pack_filter": apply_tech_pack_filter,
        "tech_pack_filter_field_value": tech_pack_filter_field_value,
        "tech_pack_row_matches_filter": tech_pack_row_matches_filter,
        "get_active_tech_pack_filters": get_active_tech_pack_filters,
        "add_tech_pack_filter_row": add_tech_pack_filter_row,
        "remove_tech_pack_filter_row": remove_tech_pack_filter_row,
        "clear_tech_pack_filters": clear_tech_pack_filters,
        "commit_tech_pack_search_query": commit_tech_pack_search_query,
        "on_tech_pack_search_submit": on_tech_pack_search_submit,
        "schedule_tech_pack_filter_apply": schedule_tech_pack_filter_apply,
        "run_tech_pack_filter_apply": run_tech_pack_filter_apply,
        "sync_tech_pack_search_query": sync_tech_pack_search_query,
        "update_tech_pack_filter_input_state": update_tech_pack_filter_input_state,
        "refresh_tech_pack_tree": refresh_tech_pack_tree,
        "update_tech_pack_tree_check_cell": update_tech_pack_tree_check_cell,
        "on_tech_pack_heading_click": on_tech_pack_heading_click,
        "on_tech_pack_tree_click": on_tech_pack_tree_click,
        "uncheck_tech_pack_style": uncheck_tech_pack_style,
        "on_tech_pack_item_selected": on_tech_pack_item_selected,
        "on_tech_pack_item_double_click": on_tech_pack_item_double_click,
        "get_selected_tech_pack_row": get_selected_tech_pack_row,
        "collect_existing_tech_pack_season_options": collect_existing_tech_pack_season_options,
        "open_add_tech_pack_style_dialog": open_add_tech_pack_style_dialog,
        "open_edit_selected_tech_pack_season_dialog": open_edit_selected_tech_pack_season_dialog,
        "get_tech_pack_image_search_text": get_tech_pack_image_search_text,
        "build_tech_pack_image_signature": build_tech_pack_image_signature,
        "get_cached_tech_pack_cover_signature": get_cached_tech_pack_cover_signature,
        "calculate_tech_pack_image_similarity": calculate_tech_pack_image_similarity,
        "focus_tech_pack_style_in_list": focus_tech_pack_style_in_list,
        "show_tech_pack_image_search_results": show_tech_pack_image_search_results,
        "load_tech_pack_image_from_clipboard": load_tech_pack_image_from_clipboard,
        "load_tech_pack_image_from_path": load_tech_pack_image_from_path,
        "run_tech_pack_image_search": run_tech_pack_image_search,
        "open_tech_pack_image_search_dialog": open_tech_pack_image_search_dialog,
        "search_tech_pack_by_image": search_tech_pack_by_image,
        "set_sync_tech_pack_running": set_sync_tech_pack_running,
        "sync_tech_pack_styles": sync_tech_pack_styles,
        "get_tech_pack_sync_search_style_code": get_tech_pack_sync_search_style_code,
        "parse_sync_tech_pack_report_paths": parse_sync_tech_pack_report_paths,
        "reconcile_synced_tech_pack_style_alias": reconcile_synced_tech_pack_style_alias,
        "_run_sync_tech_pack_worker": _run_sync_tech_pack_worker,
        "finish_sync_tech_pack": finish_sync_tech_pack,
        "sync_selected_tech_pack_styles": sync_selected_tech_pack_styles,
    }
    for method_name, function in method_map.items():
        setattr(app_cls, method_name, function)
