from __future__ import annotations

from copy import deepcopy
import threading
import json
import os
import re
import sys
import traceback
import tkinter as tk
import uuid
import webbrowser
from datetime import datetime
from pathlib import Path
from tkinter import filedialog, messagebox, ttk
from urllib.parse import urlparse

from PIL import Image, ImageTk
from app_core.window_icon import apply_window_icon as apply_common_window_icon

from app_core.paths import DEBUG_DIR, DOWNLOAD_TECH_PACK_DATA_DIR
from app_core.ui_helpers import configure_striped_treeview, create_contained_photo, resolve_image_source
from database import normalize_style_identifier

TECH_PACK_DETAIL_TABS = [
    ("overview", "tech_pack_detail_tab_overview"),
    ("bom", "tech_pack_detail_tab_bom"),
    ("constructions", "tech_pack_detail_tab_constructions"),
    ("instructions", "tech_pack_detail_tab_instructions"),
    ("stitching", "tech_pack_detail_tab_stitching"),
    ("pom", "tech_pack_detail_tab_pom"),
    ("consumption", "tech_pack_detail_tab_consumption"),
    ("samples", "tech_pack_detail_tab_samples"),
    ("attachments", "tech_pack_detail_tab_attachments"),
    ("more", "tech_pack_detail_tab_more"),
]

TECH_PACK_CONSUMPTION_VERSION_TEMPLATES = [
    ("costing", "Costing"),
    ("sample", "Sample"),
    ("booking", "Booking"),
    ("production", "Production"),
    ("settlement", "Settlement"),
]
TECH_PACK_CONSUMPTION_PIECE_UOM_OPTIONS = ["EA", "SET", "CONE"]
TECH_PACK_CONSUMPTION_LENGTH_UOM_OPTIONS = ["MM", "CM", "M", "INCH", "YARD"]
TECH_PACK_CONSUMPTION_WEIGHT_UOM_OPTIONS = ["G", "KG", "LB", "OZ"]


def _consumption_uom_options_for_item_type(item_type: object, current_value: object = "") -> list[str]:
    item_type_key = str(item_type or "").strip().lower()
    if item_type_key == "fabric":
        values = list(TECH_PACK_CONSUMPTION_LENGTH_UOM_OPTIONS + TECH_PACK_CONSUMPTION_WEIGHT_UOM_OPTIONS)
    elif item_type_key in {"trim", "trims", "label", "packaging", "artwork"}:
        values = list(TECH_PACK_CONSUMPTION_PIECE_UOM_OPTIONS + TECH_PACK_CONSUMPTION_LENGTH_UOM_OPTIONS)
    else:
        values = list(TECH_PACK_CONSUMPTION_PIECE_UOM_OPTIONS + TECH_PACK_CONSUMPTION_LENGTH_UOM_OPTIONS)
    current_text = str(current_value or "").strip().upper()
    if current_text and current_text not in values:
        values.insert(0, current_text)
    return values


def _build_consumption_costing_id(style_code: object, version_type: object, line_index: int, line_token: object = "") -> str:
    normalized_style = normalize_style_identifier(style_code) or "STYLE"
    version_token = re.sub(r"[^A-Z0-9]+", "", str(version_type or "").upper()) or "GEN"
    token = re.sub(r"[^A-Z0-9]+", "", str(line_token or "").upper())[:6]
    suffix = f"-{token}" if token else ""
    return f"{normalized_style}-{version_token}-{line_index:03d}{suffix}"


def _style_code_matches_bom_style_list(bom_style_code: object, style_code: object) -> bool:
    normalized_style = normalize_style_identifier(style_code)
    if not normalized_style:
        return True
    tokens = [
        normalize_style_identifier(token)
        for token in re.split(r"[,;/\n]+", str(bom_style_code or ""))
    ]
    tokens = [token for token in tokens if token]
    return not tokens or normalized_style in tokens


def _get_consumption_line_bom_match(self, line: dict, style_code: object = "") -> dict:
    normalized_style = normalize_style_identifier(style_code)
    bom_item_id = str(line.get("bom_item_id", "")).strip()
    bom_item_code = str(line.get("bom_item_code", "")).strip()
    fallback_match: dict = {}
    for item in list(getattr(self, "bom_items", []) or []):
        if not isinstance(item, dict):
            continue
        if bom_item_id and str(item.get("item_id", "")).strip() == bom_item_id:
            return dict(item)
        if bom_item_code and str(item.get("item_code", "")).strip() == bom_item_code:
            if not normalized_style or _style_code_matches_bom_style_list(item.get("style_code"), normalized_style):
                return dict(item)
            if not fallback_match:
                fallback_match = dict(item)
    return fallback_match


def _consumption_line_is_bom_group(line: dict) -> bool:
    raw_payload = str(line.get("source_payload_json", "") or "").strip()
    if not raw_payload:
        return False
    try:
        payload = json.loads(raw_payload)
    except (TypeError, ValueError, json.JSONDecodeError):
        return False
    if not isinstance(payload, dict):
        return False
    return str(payload.get("source", "") or "").strip() in {
        "bom-approved-consumption-group-v1",
        "bulk-trims-group-v1",
    }


def _build_consumption_line_from_bom(selected_item: dict, *, style_code: object, version_type: object, line_index: int, line_id: object = "") -> dict:
    normalized_selected = dict(selected_item or {})
    resolved_line_id = str(line_id or "").strip() or uuid.uuid4().hex
    resolved_uom = str(normalized_selected.get("purchase_uom", "")).strip() or str(normalized_selected.get("uom", "")).strip()
    return {
        "line_id": resolved_line_id,
        "costing_id": _build_consumption_costing_id(style_code, version_type, line_index, resolved_line_id),
        "bom_item_id": str(normalized_selected.get("item_id", "")).strip(),
        "bom_item_code": str(normalized_selected.get("item_code", "")).strip(),
        "bom_item_name": str(normalized_selected.get("item_name", "")).strip(),
        "fabric_type": str(normalized_selected.get("item_type", "")).strip() or str(normalized_selected.get("section", "")).strip(),
        "section": str(normalized_selected.get("section", "")).strip(),
        "color": str(normalized_selected.get("color", "")).strip(),
        "trim_size_um": str(normalized_selected.get("trim_size_um", "")).strip(),
        "length_value": str(normalized_selected.get("length_value", "")).strip(),
        "width_value": str(normalized_selected.get("width_value", "")).strip(),
        "gsm_value": str(normalized_selected.get("gsm_value", "")).strip(),
        "waste_percent": str(normalized_selected.get("waste_percent", "")).strip(),
        "uom": resolved_uom,
        "consumption": "",
    }


def _enrich_consumption_line_from_bom(self, line: dict, style_code: object = "", version_type: object = "", line_index: int = 1) -> dict:
    normalized_line = dict(line or {})
    matched_item = _get_consumption_line_bom_match(self, normalized_line, style_code)
    if matched_item:
        bom_snapshot = _build_consumption_line_from_bom(
            matched_item,
            style_code=style_code,
            version_type=version_type,
            line_index=line_index,
            line_id=normalized_line.get("line_id", ""),
        )
        bom_snapshot["consumption"] = str(normalized_line.get("consumption", "")).strip()
        bom_snapshot["costing_id"] = str(normalized_line.get("costing_id", "")).strip() or bom_snapshot["costing_id"]
        bom_snapshot["fabric_weight"] = str(normalized_line.get("fabric_weight", "")).strip()
        bom_snapshot["fabric_weight_uom"] = str(normalized_line.get("fabric_weight_uom", "")).strip()
        if not str(bom_snapshot.get("uom", "")).strip():
            bom_snapshot["uom"] = str(normalized_line.get("uom", "")).strip()
        if _consumption_line_is_bom_group(normalized_line):
            for key in (
                "bom_item_name",
                "fabric_type",
                "section",
                "color",
                "trim_size_um",
                "length_value",
                "length_unit",
                "width_value",
                "gsm_value",
                "waste_percent",
                "uom",
                "fabric_weight",
                "fabric_weight_uom",
                "source_payload_json",
            ):
                if key in normalized_line:
                    bom_snapshot[key] = str(normalized_line.get(key, "") or "").strip()
        return bom_snapshot
    return {
        "line_id": str(normalized_line.get("line_id", "")).strip() or uuid.uuid4().hex,
        "costing_id": str(normalized_line.get("costing_id", "")).strip() or _build_consumption_costing_id(
            style_code,
            version_type,
            line_index,
            normalized_line.get("line_id", ""),
        ),
        "bom_item_id": str(normalized_line.get("bom_item_id", "")).strip(),
        "bom_item_code": str(normalized_line.get("bom_item_code", "")).strip(),
        "bom_item_name": str(normalized_line.get("bom_item_name", "")).strip(),
        "fabric_type": str(normalized_line.get("fabric_type", "")).strip(),
        "section": str(normalized_line.get("section", "")).strip(),
        "color": str(normalized_line.get("color", "")).strip(),
        "trim_size_um": str(normalized_line.get("trim_size_um", "")).strip(),
        "length_value": str(normalized_line.get("length_value", "")).strip(),
        "width_value": str(normalized_line.get("width_value", "")).strip(),
        "gsm_value": str(normalized_line.get("gsm_value", "")).strip(),
        "waste_percent": str(normalized_line.get("waste_percent", "")).strip(),
        "uom": str(normalized_line.get("uom", "")).strip(),
        "consumption": str(normalized_line.get("consumption", "")).strip(),
        "fabric_weight": str(normalized_line.get("fabric_weight", "")).strip(),
        "fabric_weight_uom": str(normalized_line.get("fabric_weight_uom", "")).strip(),
    }


def sync_bom_purchase_uom_from_consumption_versions(self, consumption_versions: list[dict]) -> int:
    if not hasattr(self, "save_bom_item_to_source"):
        return 0
    bom_rows = [dict(item) for item in list(getattr(self, "bom_items", []) or []) if isinstance(item, dict)]
    if not bom_rows and hasattr(self, "load_bom_items_from_source"):
        try:
            bom_rows = [dict(item) for item in list(self.load_bom_items_from_source() or []) if isinstance(item, dict)]
            self.bom_items = list(bom_rows)
        except Exception:
            return 0
    if not bom_rows:
        return 0

    bom_by_id = {
        str(item.get("item_id", "")).strip(): dict(item)
        for item in bom_rows
        if str(item.get("item_id", "")).strip()
    }
    bom_by_code: dict[str, dict] = {}
    for item in bom_rows:
        item_code = str(item.get("item_code", "")).strip()
        if item_code and item_code not in bom_by_code:
            bom_by_code[item_code] = dict(item)

    desired_uom_by_item_id: dict[str, str] = {}
    for version in list(consumption_versions or []):
        if not isinstance(version, dict):
            continue
        for line in list(version.get("lines", [])):
            if not isinstance(line, dict):
                continue
            target_uom = str(line.get("uom", "")).strip().upper()
            if not target_uom:
                continue
            bom_item_id = str(line.get("bom_item_id", "")).strip()
            bom_item_code = str(line.get("bom_item_code", "")).strip()
            matched = bom_by_id.get(bom_item_id) if bom_item_id else None
            if matched is None and bom_item_code:
                matched = bom_by_code.get(bom_item_code)
            if not isinstance(matched, dict):
                continue
            matched_id = str(matched.get("item_id", "")).strip()
            if matched_id:
                desired_uom_by_item_id[matched_id] = target_uom

    updated_count = 0
    updated_rows_by_id = {str(item.get("item_id", "")).strip(): dict(item) for item in bom_rows if str(item.get("item_id", "")).strip()}
    for item_id, target_uom in desired_uom_by_item_id.items():
        bom_row = updated_rows_by_id.get(item_id)
        if not isinstance(bom_row, dict):
            continue
        current_purchase_uom = str(bom_row.get("purchase_uom", "")).strip().upper()
        current_uom = str(bom_row.get("uom", "")).strip().upper()
        if current_purchase_uom == target_uom and current_uom == target_uom:
            continue
        payload = dict(bom_row)
        payload["purchase_uom"] = target_uom
        payload["uom"] = target_uom
        try:
            saved = self.save_bom_item_to_source(payload)
        except Exception:
            continue
        if isinstance(saved, dict) and saved:
            updated_rows_by_id[item_id] = dict(saved)
            updated_count += 1

    if updated_count:
        self.bom_items = [
            updated_rows_by_id.get(str(item.get("item_id", "")).strip(), dict(item))
            for item in bom_rows
            if isinstance(item, dict)
        ]
    return updated_count

TECH_PACK_SAMPLES_LINK_COLUMNS = (
    "created_at",
    "request_number",
    "mr_pic",
    "pattern_pic",
    "sewer_pic",
    "ie_pic",
    "sample_type",
    "color",
    "size",
    "quantity",
    "finishing_date",
    "buyer_comment_date",
)


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


def _apply_window_icon(window: tk.Misc) -> None:
    apply_common_window_icon(window, default=False)


def _resolve_tech_pack_attachment_target(self, source: str) -> str:
    raw_value = str(source or "").strip()
    if not raw_value:
        return ""
    parsed = urlparse(raw_value)
    if parsed.scheme in {"http", "https", "file"}:
        return raw_value
    candidate = Path(raw_value)
    if candidate.exists():
        return str(candidate)
    normalized = raw_value.replace("\\", "/").lstrip("/")
    if normalized.startswith("shared/"):
        base_url = str(getattr(self, "server_settings", {}).get("shared_files_base_url", "")).strip().rstrip("/")
        if base_url:
            suffix = normalized.removeprefix("shared/").lstrip("/")
            return f"{base_url}/{suffix}"
    return raw_value


def open_tech_pack_attachment_file(self, source: str) -> None:
    target = _resolve_tech_pack_attachment_target(self, source)
    if not target:
        messagebox.showwarning(self.t("warning_title"), "Attachment is empty.")
        return
    parsed = urlparse(target)
    try:
        if parsed.scheme in {"http", "https", "file"}:
            webbrowser.open_new_tab(target)
        else:
            os.startfile(target)  # type: ignore[attr-defined]
    except Exception as exc:
        messagebox.showerror(self.t("warning_title"), str(exc))
        self.set_status(str(exc))


def _tech_pack_attachment_source_value(item: dict) -> str:
    for key in ("source_file", "file_url", "url", "path", "file_path", "attachment_path"):
        value = str(item.get(key, "") or "").strip()
        if value:
            return value
    return ""


def _refresh_tech_pack_detail_attachments_tab(self, row: dict, detail_state: dict) -> None:
    parent = detail_state.get("attachments_tab_parent") if isinstance(detail_state, dict) else None
    if parent is None:
        return
    try:
        for child in parent.winfo_children():
            child.destroy()
        self.build_tech_pack_attachments_tab(parent, row, detail_state)
    except Exception:
        pass


def _tech_pack_detail_message_parent(self, detail_state: dict | None = None):
    parent = detail_state.get("detail_window") if isinstance(detail_state, dict) else None
    try:
        if parent is not None and int(parent.winfo_exists()) == 1:
            return parent
    except Exception:
        pass
    return getattr(self, "root", None)


def _save_tech_pack_detail_attachment_changes(self, row: dict, detail_state: dict, changes: dict, status_message: str) -> bool:
    if not self.is_action_allowed("action.tech_pack.detail_save"):
        return False
    style_code = normalize_style_identifier(row.get("style_code"))
    if not style_code:
        messagebox.showwarning(self.t("warning_title"), "style_code is required.", parent=_tech_pack_detail_message_parent(self, detail_state))
        return False
    try:
        saved_record = self.save_tech_pack_record_patch_to_source(style_code, changes)
    except Exception as exc:
        messagebox.showerror(self.t("warning_title"), str(exc), parent=_tech_pack_detail_message_parent(self, detail_state))
        self.set_status(str(exc))
        return False
    library_by_style = {
        str(item.get("style_code", "")).strip(): dict(item)
        for item in list(getattr(self, "tech_pack_library", []) or [])
        if str(item.get("style_code", "")).strip()
    }
    library_by_style[style_code] = dict(saved_record)
    self.tech_pack_library = list(library_by_style.values())
    row.clear()
    row.update(saved_record)
    self.refresh_tech_pack_module()
    _refresh_tech_pack_detail_attachments_tab(self, row, detail_state)
    self.set_status(status_message)
    return True


def _save_tech_pack_detail_attachment_changes_async(self, row: dict, detail_state: dict, changes: dict, status_message: str) -> None:
    if not self.is_action_allowed("action.tech_pack.detail_save"):
        return
    style_code = normalize_style_identifier(row.get("style_code"))
    parent = _tech_pack_detail_message_parent(self, detail_state)
    if not style_code:
        messagebox.showwarning(self.t("warning_title"), "style_code is required.", parent=parent)
        return
    self.set_status("Updating Tech Pack attachment...")

    def worker() -> None:
        try:
            saved_record = self.save_tech_pack_record_patch_to_source(style_code, changes)
            result = {"ok": True, "saved_record": saved_record}
        except Exception as exc:
            result = {"ok": False, "error": str(exc)}

        def apply_result() -> None:
            if not result.get("ok"):
                error_text = str(result.get("error", "")).strip()
                messagebox.showerror(self.t("warning_title"), error_text, parent=_tech_pack_detail_message_parent(self, detail_state))
                self.set_status(error_text)
                return
            saved_record = dict(result.get("saved_record", {}) or {})
            library_by_style = {
                str(item.get("style_code", "")).strip(): dict(item)
                for item in list(getattr(self, "tech_pack_library", []) or [])
                if str(item.get("style_code", "")).strip()
            }
            library_by_style[style_code] = saved_record
            self.tech_pack_library = list(library_by_style.values())
            row.clear()
            row.update(saved_record)
            self.refresh_tech_pack_module()
            _refresh_tech_pack_detail_attachments_tab(self, row, detail_state)
            detail_parent = _tech_pack_detail_message_parent(self, detail_state)
            try:
                if detail_parent is not None:
                    detail_parent.deiconify()
                    detail_parent.lift()
                    detail_parent.focus_force()
            except Exception:
                pass
            self.set_status(status_message)

        self.schedule_root_after(0, apply_result)

    threading.Thread(target=worker, daemon=True, name="tech-pack-attachment-delete").start()


def delete_tech_pack_source_file(self, row: dict, source_file: str, detail_state: dict, source_index: int = -1) -> None:
    target = str(source_file or "").strip()
    if not target:
        return
    display_name = Path(target.replace("\\", "/")).name or target
    parent = _tech_pack_detail_message_parent(self, detail_state)
    if not messagebox.askyesno(self.t("warning_title"), f"Delete this imported file reference?\n\n{display_name}", parent=parent):
        return
    source_files = [str(item).strip() for item in list(row.get("source_files", [])) if str(item).strip()]
    remove_index = source_index if 0 <= source_index < len(source_files) and source_files[source_index] == target else -1
    if remove_index < 0:
        remove_index = next((index for index, item in enumerate(source_files) if item == target), -1)
    updated_files = [
        item
        for index, item in enumerate(source_files)
        if index != remove_index and item != target
    ]
    changes: dict[str, object] = {"source_files": updated_files}
    source_signatures = [str(item).strip() for item in list(row.get("source_file_signatures", [])) if str(item).strip()]
    if 0 <= remove_index < len(source_signatures):
        changes["source_file_signatures"] = [
            signature
            for index, signature in enumerate(source_signatures)
            if index != remove_index
        ]
    _save_tech_pack_detail_attachment_changes_async(
        self,
        row,
        detail_state,
        changes,
        "Tech Pack source file deleted.",
    )


def upload_tech_pack_attachment_page(self, row: dict, detail_state: dict) -> None:
    if not self.is_action_allowed("action.tech_pack.detail_save"):
        return
    path = filedialog.askopenfilename(
        parent=detail_state.get("detail_window") if isinstance(detail_state, dict) else None,
        title="Upload Attachment Page",
        filetypes=[("All files", "*.*")],
    )
    if not path:
        return
    source_path = Path(path)
    self.set_status(f"Uploading attachment: {source_path.name}")
    uploaded_ref = self.upload_shared_file_reference(str(source_path), bucket="tech-pack-attachments") if hasattr(self, "upload_shared_file_reference") else str(source_path)
    attachments = [dict(item) for item in list(row.get("attachments_data", [])) if isinstance(item, dict)]
    attachments.append(
        {
            "title": source_path.name,
            "source_file": uploaded_ref,
            "file_name": source_path.name,
            "uploaded_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
        }
    )
    _save_tech_pack_detail_attachment_changes(
        self,
        row,
        detail_state,
        {"attachments_data": attachments},
        f"Attachment uploaded: {source_path.name}",
    )


def delete_tech_pack_attachment_page(self, row: dict, attachment_index: int, detail_state: dict) -> None:
    attachments = [dict(item) for item in list(row.get("attachments_data", [])) if isinstance(item, dict)]
    if attachment_index < 0 or attachment_index >= len(attachments):
        return
    item = attachments[attachment_index]
    title = str(item.get("title", "")).strip() or str(item.get("file_name", "")).strip() or f"Attachment {attachment_index + 1}"
    parent = _tech_pack_detail_message_parent(self, detail_state)
    if not messagebox.askyesno(self.t("warning_title"), f"Delete this attachment?\n\n{title}", parent=parent):
        return
    del attachments[attachment_index]
    _save_tech_pack_detail_attachment_changes_async(
        self,
        row,
        detail_state,
        {"attachments_data": attachments},
        "Attachment deleted.",
    )


def append_tech_pack_detail_ui_debug_log(self, message: str) -> None:
    try:
        DEBUG_DIR.mkdir(parents=True, exist_ok=True)
        log_path = DEBUG_DIR / "tech_pack_detail_debug.log"
        with log_path.open("a", encoding="utf-8") as handle:
            handle.write(f"{datetime.now().isoformat(timespec='seconds')} {message}\n")
    except Exception:
        pass


def get_tech_pack_relationship_context(self, row: dict) -> dict:
    style_code = normalize_style_identifier(row.get("style_code"))
    library_by_style = {
        str(item.get("style_code", "")).strip(): dict(item)
        for item in list(getattr(self, "tech_pack_library", []) or [])
        if str(item.get("style_code", "")).strip()
    }
    record = dict(library_by_style.get(style_code, row if isinstance(row, dict) else {}))
    overview_data = record.get("overview_data", {}) if isinstance(record.get("overview_data", {}), dict) else {}
    raw_parent_value = str(overview_data.get("lead_style_no", "")).strip()
    normalized_parent_style = normalize_style_identifier(raw_parent_value)

    is_lead = raw_parent_value.lower() == "lead" or normalized_parent_style == style_code
    parent_style_code = ""
    if not is_lead and normalized_parent_style and normalized_parent_style != style_code:
        parent_style_code = normalized_parent_style
    parent_record = dict(library_by_style.get(parent_style_code, {})) if parent_style_code else {}

    child_rows: list[dict] = []
    for candidate in library_by_style.values():
        candidate_style_code = normalize_style_identifier(candidate.get("style_code"))
        if not candidate_style_code or candidate_style_code == style_code:
            continue
        candidate_overview = candidate.get("overview_data", {}) if isinstance(candidate.get("overview_data", {}), dict) else {}
        candidate_parent = normalize_style_identifier(candidate_overview.get("lead_style_no"))
        if candidate_parent == style_code:
            child_rows.append(dict(candidate))
    child_rows.sort(key=lambda item: str(item.get("style_code", "")).strip().lower())

    available_style_codes = sorted(
        {
            str(item.get("style_code", "")).strip()
            for item in library_by_style.values()
            if str(item.get("style_code", "")).strip() and str(item.get("style_code", "")).strip() != style_code
        },
        key=str.lower,
    )

    return {
        "style_code": style_code,
        "record": record,
        "overview_data": overview_data,
        "raw_parent_value": raw_parent_value,
        "is_lead": is_lead,
        "is_baby": bool(parent_style_code),
        "parent_style_code": parent_style_code,
        "parent_record": parent_record,
        "child_rows": child_rows,
        "available_style_codes": available_style_codes,
    }


def upsert_tech_pack_detail_library_record(self, record: dict) -> dict:
    if not isinstance(record, dict):
        return {}
    style_code = normalize_style_identifier(record.get("style_code"))
    if not style_code:
        return dict(record)
    normalized_record = self.normalize_tech_pack_record(record) if hasattr(self, "normalize_tech_pack_record") else dict(record)
    if not isinstance(normalized_record, dict):
        normalized_record = dict(record)
    normalized_record["style_code"] = style_code

    current_rows = [
        dict(item)
        for item in list(getattr(self, "tech_pack_library", []) or [])
        if isinstance(item, dict)
    ]
    replaced = False
    for row_index, current_row in enumerate(current_rows):
        if normalize_style_identifier(current_row.get("style_code")) == style_code:
            current_rows[row_index] = normalized_record
            replaced = True
            break
    if not replaced:
        current_rows.append(normalized_record)
    self.tech_pack_library = current_rows

    try:
        self.tech_pack_rows = self.build_tech_pack_rows()
        if hasattr(self, "apply_tech_pack_filter"):
            self.apply_tech_pack_filter()
        elif hasattr(self, "update_tech_pack_module_status"):
            self.update_tech_pack_module_status()
    except Exception:
        pass
    return normalized_record


def get_tech_pack_effective_detail_row(self, row: dict, tab_key: str) -> dict:
    relationship = self.get_tech_pack_relationship_context(row)
    if not relationship["is_baby"] or not relationship["parent_record"]:
        return row
    if tab_key not in {"constructions", "instructions", "stitching", "pom"}:
        return row

    parent_record = dict(relationship["parent_record"])
    effective_row = dict(row)
    if tab_key == "constructions":
        effective_row["construction_data"] = deepcopy(parent_record.get("construction_data", []))
        effective_row["construction_summary"] = str(parent_record.get("construction_summary", "")).strip()
    elif tab_key == "instructions":
        effective_row["design_intent_data"] = deepcopy(parent_record.get("design_intent_data", []))
        effective_row["attachments_data"] = deepcopy(parent_record.get("attachments_data", []))
        effective_row["instructions_notes"] = str(parent_record.get("instructions_notes", "")).strip()
    elif tab_key == "stitching":
        effective_row["stitching_data"] = deepcopy(parent_record.get("stitching_data", []))
        effective_row["stitching_notes"] = str(parent_record.get("stitching_notes", "")).strip()
    elif tab_key == "pom":
        effective_row["pom_data"] = deepcopy(parent_record.get("pom_data", []))
        effective_row["pom_notes"] = str(parent_record.get("pom_notes", "")).strip()
    effective_row["_inherited_from_style"] = relationship["parent_style_code"]
    return effective_row


def open_add_tech_pack_baby_style_dialog(self, row: dict, detail_state: dict, anchor_widget: tk.Widget | None = None) -> None:
    relationship = self.get_tech_pack_relationship_context(row)
    style_code = str(relationship["style_code"]).strip()
    if not style_code:
        return

    existing_children = {
        str(item.get("style_code", "")).strip()
        for item in relationship["child_rows"]
        if str(item.get("style_code", "")).strip()
    }

    parent_window = detail_state.get("detail_window") if isinstance(detail_state, dict) else None
    try:
        if parent_window is None or int(parent_window.winfo_exists()) != 1:
            parent_window = self.root
    except Exception:
        parent_window = self.root

    dialog = tk.Toplevel(parent_window)
    dialog.title(f"Add Baby Style - {style_code}")
    dialog.transient(parent_window)
    dialog.resizable(True, True)

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

    ttk.Label(
        frame,
        text=f"Type one baby style per row for lead style {style_code}. Missing styles will be created automatically.",
        wraplength=520,
        justify="left",
    ).grid(row=0, column=0, sticky="w")

    table_host = ttk.Frame(frame)
    table_host.grid(row=1, column=0, sticky="nsew", pady=(10, 0))
    table_host.columnconfigure(0, weight=1)

    entry_vars: list[tk.StringVar] = []
    entry_widgets: list[ttk.Entry] = []

    def add_input_row(initial_value: object = "") -> None:
        row_index = len(entry_vars) + 1
        row_frame = ttk.Frame(table_host)
        row_frame.grid(row=row_index, column=0, sticky="ew", pady=(0, 6))
        row_frame.columnconfigure(1, weight=1)
        ttk.Label(row_frame, text=str(row_index), width=4).grid(row=0, column=0, sticky="w", padx=(0, 6))
        value_var = tk.StringVar(value=str(initial_value or "").strip())
        entry = ttk.Entry(row_frame, textvariable=value_var, width=32)
        entry.grid(row=0, column=1, sticky="ew")
        entry_vars.append(value_var)
        entry_widgets.append(entry)

    header = ttk.Frame(table_host)
    header.grid(row=0, column=0, sticky="ew", pady=(0, 6))
    ttk.Label(header, text="#", width=4).grid(row=0, column=0, sticky="w", padx=(0, 6))
    ttk.Label(header, text="Baby Style").grid(row=0, column=1, sticky="w")
    for _index in range(12):
        add_input_row()

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

    def save_selection() -> None:
        requested_styles: list[str] = []
        seen_styles: set[str] = set()
        for variable in entry_vars:
            child_style_code = normalize_style_identifier(variable.get())
            if not child_style_code or child_style_code in seen_styles:
                continue
            seen_styles.add(child_style_code)
            requested_styles.append(child_style_code)
        if not requested_styles:
            messagebox.showwarning(self.t("warning_title"), "Please type at least one baby style.")
            return
        parent_overview = relationship["overview_data"]
        for widget in entry_widgets:
            widget.configure(state="disabled")
        save_button.configure(state="disabled")
        cancel_button.configure(state="disabled")
        add_row_button.configure(state="disabled")
        self.set_status(f"Saving baby style links for {style_code}...")

        def worker() -> None:
            payload: dict[str, object]
            saved_records: list[dict] = []
            fallback_records: list[dict] = []
            saved_styles: list[str] = []
            skipped_styles: list[str] = []
            worker_existing_children = set(existing_children)
            try:
                for child_style_code in requested_styles:
                    if child_style_code == style_code or child_style_code in worker_existing_children:
                        skipped_styles.append(child_style_code)
                        continue
                    child_relationship = self.get_tech_pack_relationship_context({"style_code": child_style_code})
                    child_record = dict(child_relationship.get("record") or self.default_tech_pack_record(child_style_code))
                    child_overview = dict(child_record.get("overview_data", {})) if isinstance(child_record.get("overview_data", {}), dict) else {}
                    child_overview["lead_style_no"] = style_code
                    child_overview["lead_season"] = str(parent_overview.get("season", "")).strip() or str(row.get("season", "")).strip()
                    child_overview["lead_season_year"] = str(parent_overview.get("season_year", "")).strip()
                    last_updated = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                    saved_record = self.save_tech_pack_record_patch_to_source(
                        child_style_code,
                        {
                            "style_code": child_style_code,
                            "overview_data": child_overview,
                            "last_updated": last_updated,
                        },
                    )
                    if isinstance(saved_record, dict):
                        saved_records.append(saved_record)
                    else:
                        child_record["style_code"] = child_style_code
                        child_record["overview_data"] = child_overview
                        child_record["last_updated"] = last_updated
                        fallback_records.append(child_record)
                    worker_existing_children.add(child_style_code)
                    saved_styles.append(child_style_code)
                payload = {
                    "ok": True,
                    "saved_records": saved_records,
                    "fallback_records": fallback_records,
                    "saved_styles": saved_styles,
                    "skipped_styles": skipped_styles,
                }
            except Exception as exc:
                payload = {
                    "ok": False,
                    "error": str(exc),
                    "traceback": traceback.format_exc(limit=8),
                }

            def apply_result() -> None:
                if not payload.get("ok"):
                    for widget in entry_widgets:
                        try:
                            widget.configure(state="normal")
                        except tk.TclError:
                            pass
                    try:
                        save_button.configure(state="normal")
                        cancel_button.configure(state="normal")
                        add_row_button.configure(state="normal")
                    except tk.TclError:
                        pass
                    error_message = str(payload.get("error") or "Failed to save baby style links.")
                    try:
                        parent_for_error = dialog if int(dialog.winfo_exists()) == 1 else parent_window
                    except Exception:
                        parent_for_error = parent_window
                    messagebox.showerror(self.t("warning_title"), error_message, parent=parent_for_error)
                    self.set_status(error_message)
                    return
                for saved_record in list(payload.get("saved_records", []) or []):
                    if isinstance(saved_record, dict):
                        self.upsert_tech_pack_detail_library_record(saved_record)
                for fallback_record in list(payload.get("fallback_records", []) or []):
                    if isinstance(fallback_record, dict):
                        self.upsert_tech_pack_detail_library_record(fallback_record)
                saved_styles = [str(item).strip() for item in list(payload.get("saved_styles", []) or []) if str(item).strip()]
                skipped_styles = [str(item).strip() for item in list(payload.get("skipped_styles", []) or []) if str(item).strip()]
                if saved_styles and hasattr(self, "start_tech_pack_module_refresh_async"):
                    self.start_tech_pack_module_refresh_async(silent=True, force=True)
                if callable(detail_state.get("refresh_baby_style_panel")):
                    detail_state["refresh_baby_style_panel"]()
                status_parts = []
                if saved_styles:
                    status_parts.append(f"Linked {len(saved_styles)} baby style(s) to lead style {style_code}: {', '.join(saved_styles[:8])}")
                if skipped_styles:
                    status_parts.append(f"Skipped existing/self style(s): {', '.join(skipped_styles[:8])}")
                self.set_status(" | ".join(status_parts) if status_parts else f"No baby style changed for {style_code}.")
                try:
                    dialog.destroy()
                except tk.TclError:
                    pass

            self.schedule_root_after(0, apply_result)

        threading.Thread(target=worker, daemon=True).start()

    add_row_button = ttk.Button(button_row, text="+ Row", command=add_input_row)
    add_row_button.grid(row=0, column=0, sticky="w")
    cancel_button = ttk.Button(button_row, text="Cancel", command=dialog.destroy)
    cancel_button.grid(row=0, column=1, sticky="e", padx=(0, 8))
    save_button = ttk.Button(button_row, text="Save", command=save_selection)
    save_button.grid(row=0, column=2, sticky="e")
    dialog.update_idletasks()
    try:
        anchor = anchor_widget or detail_state.get("baby_style_add_button")
        if anchor is not None:
            anchor.update_idletasks()
            dialog_width = dialog.winfo_width() or 560
            dialog_height = dialog.winfo_height() or 460
            x = anchor.winfo_rootx() + anchor.winfo_width() + 8
            y = anchor.winfo_rooty() - 4
            screen_width = dialog.winfo_screenwidth()
            screen_height = dialog.winfo_screenheight()
            if x + dialog_width > screen_width - 24:
                x = max(24, anchor.winfo_rootx() - dialog_width - 8)
            if y + dialog_height > screen_height - 48:
                y = max(24, screen_height - dialog_height - 48)
            dialog.geometry(f"+{max(24, x)}+{max(24, y)}")
    except Exception:
        pass
    if entry_widgets:
        entry_widgets[0].focus_set()
    try:
        dialog.lift(parent_window)
        dialog.focus_set()
    except tk.TclError:
        pass


def remove_tech_pack_baby_style_link(self, lead_row: dict, child_style_code: str, detail_state: dict | None = None) -> None:
    normalized_child_style = normalize_style_identifier(child_style_code)
    if not normalized_child_style:
        self.append_tech_pack_detail_ui_debug_log("baby-style remove skipped: empty child style")
        return
    lead_style_code = normalize_style_identifier(lead_row.get("style_code")) if isinstance(lead_row, dict) else ""
    self.append_tech_pack_detail_ui_debug_log(
        f"baby-style remove start lead={lead_style_code} child={normalized_child_style}"
    )
    child_relationship = self.get_tech_pack_relationship_context({"style_code": normalized_child_style})
    child_record = dict(child_relationship.get("record", {}))
    if not child_record:
        self.append_tech_pack_detail_ui_debug_log(
            f"baby-style remove missing-record lead={lead_style_code} child={normalized_child_style}"
        )
        messagebox.showwarning(self.t("warning_title"), f"Cannot find baby style record: {normalized_child_style}")
        self.set_status(f"Cannot find baby style record: {normalized_child_style}")
        return
    previous_record = dict(child_record)
    child_overview = dict(child_record.get("overview_data", {})) if isinstance(child_record.get("overview_data", {}), dict) else {}
    child_overview["lead_style_no"] = ""
    child_overview["lead_season"] = ""
    child_overview["lead_season_year"] = ""
    child_record["overview_data"] = child_overview
    last_updated = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    child_record["style_code"] = normalized_child_style
    child_record["last_updated"] = last_updated
    self.upsert_tech_pack_detail_library_record(child_record)
    if isinstance(detail_state, dict) and callable(detail_state.get("refresh_baby_style_panel")):
        detail_state["refresh_baby_style_panel"]()
    self.set_status(f"Removing baby style {normalized_child_style} from lead style {lead_style_code}...")
    self.append_tech_pack_detail_ui_debug_log(
        f"baby-style remove optimistic lead={lead_style_code} child={normalized_child_style}"
    )

    patch_changes = {
        "style_code": normalized_child_style,
        "overview_data": {
            "lead_style_no": "",
            "lead_season": "",
            "lead_season_year": "",
        },
        "last_updated": last_updated,
    }

    def worker() -> None:
        payload: dict[str, object]
        try:
            saved_record = self.save_tech_pack_record_patch_to_source(normalized_child_style, patch_changes)
            payload = {"ok": True, "saved_record": saved_record}
        except Exception as exc:
            payload = {
                "ok": False,
                "error": str(exc),
                "traceback": traceback.format_exc(limit=8),
            }

        def apply_result() -> None:
            if payload.get("ok"):
                saved_record = payload.get("saved_record")
                if isinstance(saved_record, dict):
                    self.upsert_tech_pack_detail_library_record(saved_record)
                else:
                    self.upsert_tech_pack_detail_library_record(child_record)
                if hasattr(self, "start_tech_pack_module_refresh_async"):
                    self.start_tech_pack_module_refresh_async(silent=True, force=True)
                if isinstance(detail_state, dict) and callable(detail_state.get("refresh_baby_style_panel")):
                    detail_state["refresh_baby_style_panel"]()
                self.set_status(f"Removed baby style {normalized_child_style} from lead style {lead_style_code}.")
                self.append_tech_pack_detail_ui_debug_log(
                    f"baby-style remove done lead={lead_style_code} child={normalized_child_style}"
                )
                return
            self.upsert_tech_pack_detail_library_record(previous_record)
            if isinstance(detail_state, dict) and callable(detail_state.get("refresh_baby_style_panel")):
                detail_state["refresh_baby_style_panel"]()
            error_message = str(payload.get("error") or "Failed to remove baby style.")
            self.append_tech_pack_detail_ui_debug_log(
                f"baby-style remove failed lead={lead_style_code} child={normalized_child_style} error={error_message}"
            )
            messagebox.showerror(self.t("warning_title"), error_message)
            self.set_status(error_message)

        self.schedule_root_after(0, apply_result)

    threading.Thread(target=worker, daemon=True).start()


def invoke_remove_tech_pack_baby_style_link(
    self,
    lead_row: dict,
    child_style_code: str,
    detail_state: dict | None = None,
    button: tk.Widget | None = None,
) -> None:
    try:
        self.append_tech_pack_detail_ui_debug_log(
            f"baby-style remove invoke child={normalize_style_identifier(child_style_code)}"
        )
        if button is not None:
            try:
                button.configure(text="...", state="disabled", relief="sunken", bg="#d9e8ff", activebackground="#c8dcff")
            except Exception:
                button.configure(state="disabled")
        self.remove_tech_pack_baby_style_link(lead_row, child_style_code, detail_state)
    except Exception as exc:
        self.append_tech_pack_detail_ui_debug_log(
            f"baby-style remove error child={normalize_style_identifier(child_style_code)} error={type(exc).__name__}: {exc}"
        )
        messagebox.showerror(self.t("warning_title"), str(exc))
        self.set_status(str(exc))
        if button is not None:
            try:
                button.configure(state="normal")
            except Exception:
                pass


def refresh_tech_pack_baby_style_panel(self, row: dict, detail_state: dict) -> None:
    host = detail_state.get("baby_style_list_host")
    if host is None:
        return
    for child in host.winfo_children():
        child.destroy()

    relationship = self.get_tech_pack_relationship_context(row)
    raw_parent_value = relationship["raw_parent_value"]
    if relationship["is_lead"]:
        child_rows = relationship["child_rows"]
        add_button = ttk.Button(
            host,
            text="+",
            width=3,
        )
        add_button.configure(command=lambda button_ref=add_button: self.open_add_tech_pack_baby_style_dialog(row, detail_state, button_ref))
        add_button.grid(row=0, column=0, sticky="w", pady=(0, 6))
        detail_state["baby_style_add_button"] = add_button
        ttk.Label(host, text="Add baby style").grid(row=0, column=1, sticky="w", padx=(6, 0), pady=(0, 6))
        if child_rows:
            grid_shell = ttk.Frame(host)
            grid_shell.grid(row=1, column=0, columnspan=2, sticky="nsew", pady=(2, 0))
            grid_shell.columnconfigure(0, weight=1)
            grid_shell.rowconfigure(0, weight=1)
            use_scrollbar = len(child_rows) > 9
            canvas_height = 110 if use_scrollbar else max(36, min(110, ((len(child_rows) + 2) // 3) * 34))
            baby_canvas = tk.Canvas(grid_shell, highlightthickness=0, height=canvas_height)
            baby_canvas.grid(row=0, column=0, sticky="nsew")
            grid_host = ttk.Frame(baby_canvas)
            canvas_window = baby_canvas.create_window((0, 0), window=grid_host, anchor="nw")
            if use_scrollbar:
                scrollbar = ttk.Scrollbar(grid_shell, orient="vertical", command=baby_canvas.yview)
                scrollbar.grid(row=0, column=1, sticky="ns", padx=(4, 0))
                baby_canvas.configure(yscrollcommand=scrollbar.set)

            for column_index in range(3):
                grid_host.columnconfigure(column_index, weight=1)

            def sync_scroll_region(_event: tk.Event | None = None) -> None:
                baby_canvas.configure(scrollregion=baby_canvas.bbox("all"))

            def sync_canvas_width(event: tk.Event) -> None:
                baby_canvas.itemconfigure(canvas_window, width=event.width)

            grid_host.bind("<Configure>", sync_scroll_region)
            baby_canvas.bind("<Configure>", sync_canvas_width)
            def on_mousewheel(event: tk.Event) -> str:
                if use_scrollbar:
                    baby_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
                return "break"

            baby_canvas.bind("<Enter>", lambda _event: baby_canvas.bind_all("<MouseWheel>", on_mousewheel))
            baby_canvas.bind("<Leave>", lambda _event: baby_canvas.unbind_all("<MouseWheel>"))
            for row_index, child_row in enumerate(child_rows, start=1):
                child_style_code = str(child_row.get("style_code", "")).strip()
                item_row = (row_index - 1) // 3
                item_col = (row_index - 1) % 3
                item_frame = ttk.Frame(grid_host)
                item_frame.grid(row=item_row, column=item_col, sticky="w", padx=(0, 10), pady=(0, 4))
                ttk.Label(item_frame, text=child_style_code, width=9).grid(row=0, column=0, sticky="w", padx=(0, 4))
                remove_button = tk.Button(
                    item_frame,
                    text="-",
                    width=3,
                    relief="raised",
                    bg="#f5f5f5",
                    activebackground="#d9e8ff",
                    cursor="hand2",
                    takefocus=True,
                )
                def trigger_remove(style_ref: str = child_style_code, button_ref: tk.Widget = remove_button) -> None:
                    self.invoke_remove_tech_pack_baby_style_link(
                        row,
                        style_ref,
                        detail_state,
                        button_ref,
                    )
                remove_button.configure(command=trigger_remove)
                remove_button.grid(row=0, column=1, sticky="w")
        else:
            ttk.Label(host, text="No baby style linked yet.", foreground="#6c748c").grid(row=1, column=0, columnspan=2, sticky="w")
    elif relationship["is_baby"]:
        remove_button = tk.Button(
            host,
            text="-",
            width=3,
            relief="raised",
            bg="#f5f5f5",
            activebackground="#d9e8ff",
            cursor="hand2",
            takefocus=True,
        )
        def trigger_remove_baby(button_ref: tk.Widget = remove_button) -> None:
            self.invoke_remove_tech_pack_baby_style_link(
                {"style_code": relationship["parent_style_code"]},
                relationship["style_code"],
                detail_state,
                button_ref,
            )

        remove_button.configure(command=trigger_remove_baby)
        remove_button.grid(row=0, column=0, sticky="w", pady=(0, 6))
        ttk.Label(
            host,
            text=f"Inherits from lead style {relationship['parent_style_code']}.",
            foreground="#6c748c",
        ).grid(row=0, column=1, sticky="w", padx=(6, 0), pady=(0, 6))
    else:
        message = "Set Lead Style No to 'Lead' or another style code."
        if raw_parent_value:
            message = f"Lead Style No '{raw_parent_value}' is not linked to a saved style yet."
        ttk.Label(host, text=message, foreground="#6c748c").grid(row=0, column=0, sticky="w")

def tech_pack_guess_overview_context(self, row: dict) -> dict:
    overview_data = row.get("overview_data", {}) if isinstance(row.get("overview_data", {}), dict) else {}
    bom_sections = [section for section in list(row.get("bom_data", [])) if isinstance(section, dict)]
    primary_material = ""
    for section in bom_sections:
        if str(section.get("section", "")).strip().upper() != "MATERIAL":
            continue
        for item in list(section.get("items", [])):
            if not isinstance(item, dict):
                continue
            primary_material = (
                str(item.get("description", "")).strip()
                or str(item.get("component", "")).strip()
            )
            if primary_material:
                break
        if primary_material:
            break

    gender = self.infer_tech_pack_gender(row, overview_data)
    size_models = [
        str(item.get("grading_model", "")).strip().replace("_", " ")
        for item in list(row.get("pom_data", []))
        if isinstance(item, dict) and str(item.get("grading_model", "")).strip()
    ]
    size_order_hints = [str(item).strip().replace("_", " ") for item in list(overview_data.get("size_ranges", [])) if str(item).strip()]
    if size_order_hints:
        def size_sort_key(value: str) -> tuple[int, str]:
            upper_value = value.upper()
            for index, hint in enumerate(size_order_hints):
                upper_hint = hint.upper().replace("_1", "").replace("_", " ")
                if upper_hint and upper_hint.split()[0] in upper_value:
                    return (index, upper_value)
            return (len(size_order_hints), upper_value)

        size_models = sorted(size_models, key=size_sort_key)
    treatments = [str(item).strip() for item in list(overview_data.get("treatments", [])) if str(item).strip()]
    colors = [item for item in list(overview_data.get("colors", [])) if isinstance(item, dict)]
    design_detail_parts: list[str] = []
    stored_design_details = str(overview_data.get("design_details", "")).strip()
    if stored_design_details:
        design_detail_parts.append(stored_design_details)
    for item in list(row.get("design_intent_data", [])):
        if not isinstance(item, dict):
            continue
        value = str(item.get("details", "")).strip()
        if value and value not in design_detail_parts:
            design_detail_parts.append(value)
    if not design_detail_parts:
        overview_notes = str(row.get("overview_notes", "")).strip()
        if overview_notes:
            design_detail_parts.extend([part.strip() for part in overview_notes.split("\n\n") if part.strip()])
    design_details = "\n\n".join(design_detail_parts)

    season_field_value = str(overview_data.get("season", "")).strip() or str(row.get("season_base", "")).strip() or str(row.get("season", "")).strip()
    season_year_field_value = str(overview_data.get("season_year", "")).strip() or str(row.get("season_year", "")).strip()
    if hasattr(self, "split_season_display"):
        season_field_value, season_year_field_value = self.split_season_display(season_field_value, season_year_field_value)

    hierarchy_fields = [
        {"key": "season", "label": "Season", "value": season_field_value},
        {"key": "season_year", "label": "Season Year", "value": season_year_field_value},
        {"key": "seasonal_attribute", "label": "Seasonal Attribute", "value": str(overview_data.get("seasonal_attribute", "")).strip()},
        {"key": "brand", "label": "Brand", "value": str(overview_data.get("brand", "")).strip() or str(row.get("collection", "")).strip()},
        {"key": "gender", "label": "Gender", "value": gender},
        {"key": "sbu_organization_code", "label": "SBU/Organization Code", "value": str(overview_data.get("sbu_organization_code", "")).strip()},
        {"key": "family_of_business", "label": "Family of Business", "value": str(overview_data.get("family_of_business", "")).strip() or gender},
        {"key": "division_display", "label": "Division", "value": str(overview_data.get("division_display", "")).strip()},
        {"key": "product_type_maj_class", "label": "Product Type/Maj Class", "value": str(overview_data.get("class_name", "")).strip()},
        {"key": "earliest_ex_factory_date", "label": "Earliest Ex-Factory Date", "value": str(overview_data.get("earliest_ex_factory_date", "")).strip()},
        {"key": "in_dc_date", "label": "In DC Date", "value": str(overview_data.get("in_dc_date", "")).strip()},
        {"key": "target_first_cost", "label": "Target First Cost", "value": str(overview_data.get("target_first_cost", "")).strip()},
        {"key": "target_qty", "label": "Target Qty", "value": str(overview_data.get("target_qty", "")).strip()},
    ]

    product_fields = [
        {"key": "program_name", "label": "Program Name", "value": self.infer_tech_pack_program_name(row, overview_data)},
        {"key": "style_code", "label": "*Style No", "value": str(row.get("style_code", "")).strip(), "readonly": True},
        {"key": "style_name", "label": "Style Description", "value": str(row.get("style_name", "")).strip()},
        {"key": "pom_match", "label": "POM Match", "value": str(overview_data.get("pom_match", "")).strip()},
        {"key": "fabric_category", "label": "Fabric Category", "value": str(overview_data.get("fabric_category", "")).strip() or ("KNIT" if "KNIT" in primary_material.upper() else "")},
        {"key": "commodity", "label": "Commodity", "value": str(overview_data.get("commodity", "")).strip()},
        {"key": "item_type", "label": "Item Type", "value": str(overview_data.get("item_type", "")).strip()},
        {"key": "primary_material", "label": "Primary Material", "value": str(overview_data.get("primary_material", "")).strip() or primary_material},
        {"key": "offers_mass_created", "label": "Offers Mass Created", "value": str(overview_data.get("offers_mass_created", "")).strip()},
    ]

    owner_fields = [
        {"key": "request_no", "label": "Request No", "value": str(overview_data.get("request_no", "")).strip()},
        {"key": "status", "label": "Status", "value": str(overview_data.get("status", "")).strip() or str(row.get("production_status", "")).strip()},
        {"key": "designer", "label": "Designer", "value": str(overview_data.get("designer", "")).strip()},
        {"key": "product_manager", "label": "Sourcing Manager", "value": str(overview_data.get("product_manager", "")).strip()},
        {"key": "tech_designer", "label": "Tech Designer", "value": str(overview_data.get("tech_designer", "")).strip()},
        {"key": "carryover_new", "label": "New/Carryover", "value": str(overview_data.get("carryover_new", "")).strip()},
        {"key": "design_status", "label": "Design Status", "value": str(overview_data.get("design_status", "")).strip()},
        {"key": "technical_status", "label": "Technical Status", "value": str(overview_data.get("technical_status", "")).strip()},
    ]

    size_rows: list[tuple[str, str, str]] = []
    for size_model in size_models[:6]:
        size_class = size_model.split()[0] if size_model else ""
        family = gender[:1].upper() if gender else ""
        size_rows.append((size_model, size_class, family))
    while len(size_rows) < 6:
        size_rows.append(("", "", ""))

    treatment_rows: list[tuple[str, str, str]] = []
    for treatment in treatments[:4]:
        treatment_rows.append((treatment, "", "--Select--"))
    while len(treatment_rows) < 4:
        treatment_rows.append(("", "", "--Select--"))

    color_rows: list[dict] = []
    for item in colors[:6]:
        color_rows.append(
            {
                "swatch": self.tech_pack_preview_swatch_color(
                    str(item.get("color_name", "")).strip(),
                    str(item.get("colorway", "")).strip(),
                ),
                "name": str(item.get("color_name", "")).strip(),
                "bealls_name": str(item.get("colorway", "")).strip(),
                "csp_ref": str(item.get("color_name", "")).strip(),
                "colorway_type": "NEW SOLID" if str(item.get("colorway", "")).strip() else "",
                "color_code": "",
                "status": "",
                "size_attributes": "",
            }
        )
    while len(color_rows) < 6:
        color_rows.append(
            {
                "swatch": "#f2f2f2",
                "name": "",
                "bealls_name": "",
                "csp_ref": "",
                "colorway_type": "--Select--" if len(color_rows) < 4 else "",
                "color_code": "",
                "status": "",
                "size_attributes": "",
            }
        )

    return {
        "owner_fields": owner_fields,
        "product_fields": product_fields,
        "hierarchy_fields": hierarchy_fields,
        "color_rows": color_rows,
        "size_rows": size_rows,
        "treatment_rows": treatment_rows,
        "design_details": design_details,
        "lead_style_fields": [
            {"key": "lead_style_no", "label": "Lead Style No", "value": str(overview_data.get("lead_style_no", "")).strip()},
            {"key": "lead_season", "label": "Season", "value": str(overview_data.get("lead_season", "")).strip()},
            {"key": "lead_season_year", "label": "Season Year", "value": str(overview_data.get("lead_season_year", "")).strip()},
        ],
        "lead_style_comments": str(overview_data.get("lead_style_comments", "")).strip(),
    }

def build_tech_pack_overview_form_field(
    self,
    parent: tk.Widget,
    row_index: int,
    column_index: int,
    field: dict,
    *,
    width: int = 28,
    row_offset: int = 1,
    detail_state: dict | None = None,
) -> None:
    label_text = str(field.get("label", "")).strip()
    value_text = str(field.get("value", "")).strip()
    highlight = bool(field.get("highlight")) or label_text in {"*Style No", "Brand", "Commodity"}
    is_readonly = bool(field.get("readonly"))
    base_row = row_offset + row_index * 2
    ttk.Label(parent, text=label_text).grid(row=base_row, column=column_index, sticky="w", pady=(0, 3))
    variable = tk.StringVar(value=value_text)
    entry = tk.Entry(
        parent,
        textvariable=variable,
        relief="solid",
        bd=1,
        font=("Segoe UI", 10),
        readonlybackground="#fff34d" if highlight else "#ededed",
        disabledbackground="#fff34d" if highlight else "#ededed",
        fg="#333333",
        width=width,
    )
    entry.grid(row=base_row + 1, column=column_index, sticky="ew", padx=(0, 10), pady=(0, 8), ipady=3)
    if is_readonly:
        entry.configure(state="readonly")
    else:
        entry.configure(state="normal" if detail_state and detail_state.get("edit_mode") else "readonly")
    if detail_state is not None and str(field.get("key", "")).strip():
        detail_state.setdefault("field_vars", {})[str(field["key"])] = variable
        detail_state.setdefault("field_meta", {})[str(field["key"])] = dict(field)
        detail_state.setdefault("field_entries", []).append((entry, is_readonly))

def build_tech_pack_overview_textarea(
    self,
    parent: tk.Widget,
    *,
    title: str,
    field_key: str,
    value: str,
    width: int = 32,
    height: int = 7,
    detail_state: dict | None = None,
    readonly: bool = False,
) -> ttk.Frame:
    frame = ttk.Frame(parent)
    frame.columnconfigure(0, weight=1)
    text_row = 0
    if title:
        ttk.Label(frame, text=title).grid(row=0, column=0, sticky="w", pady=(0, 4))
        text_row = 1
    text_widget = tk.Text(
        frame,
        wrap="word",
        height=height,
        width=width,
        relief="solid",
        bd=1,
        font=("Segoe UI", 10),
        bg="#ededed",
        fg="#333333",
    )
    text_widget.grid(row=text_row, column=0, sticky="nsew")
    text_widget.insert("1.0", value)
    if readonly or not (detail_state and detail_state.get("edit_mode")):
        text_widget.configure(state="disabled")
    if detail_state is not None and field_key:
        detail_state.setdefault("text_widgets", {})[field_key] = text_widget
        detail_state.setdefault("text_entries", []).append((text_widget, readonly))
    return frame

def build_tech_pack_overview_section_fields(
    self,
    parent: tk.Widget,
    title: str,
    fields: list[dict],
    *,
    columns: int = 1,
    wide_label: str = "",
    detail_state: dict | None = None,
) -> ttk.Frame:
    section = ttk.Frame(parent)
    for column_index in range(columns):
        section.columnconfigure(column_index, weight=1)
    tk.Label(section, text=title, font=("Segoe UI", 15, "bold"), fg="#4a4a4a").grid(
        row=0, column=0, columnspan=columns, sticky="w", pady=(0, 10)
    )
    for index, field in enumerate(fields):
        col = index % columns
        row_index = index // columns
        label_text = str(field.get("label", "")).strip()
        self.build_tech_pack_overview_form_field(
            section,
            row_index,
            col,
            field,
            width=32 if label_text == wide_label else 26,
            row_offset=1,
            detail_state=detail_state,
        )
    return section

def build_tech_pack_overview_table_cell(
    self,
    parent: tk.Widget,
    row_index: int,
    column_index: int,
    value_text: str = "",
    *,
    bg: str = "#ededed",
    width: int = 16,
    anchor: str = "w",
) -> tk.Label:
    label = tk.Label(
        parent,
        text=value_text,
        font=("Segoe UI", 9),
        bg=bg,
        fg="#333333",
        relief="solid",
        bd=1,
        width=width,
        anchor=anchor,
        padx=6,
        pady=4,
    )
    label.grid(row=row_index, column=column_index, sticky="nsew", padx=(0, 6), pady=(0, 6))
    return label

def build_tech_pack_bom_grid_cell(
    self,
    parent: tk.Widget,
    row_index: int,
    column_index: int,
    value_text: str = "",
    *,
    bg: str = "#ededed",
    width: int = 12,
    anchor: str = "w",
    padx: tuple[int, int] = (0, 2),
    pady: tuple[int, int] = (0, 2),
) -> tk.Label:
    label = tk.Label(
        parent,
        text=value_text,
        font=("Segoe UI", 8),
        bg=bg,
        fg="#333333",
        relief="solid",
        bd=1,
        width=width,
        anchor=anchor,
        padx=4,
        pady=3,
    )
    label.grid(row=row_index, column=column_index, sticky="nsew", padx=padx, pady=pady)
    return label

def get_tech_pack_color_lookup(self, row: dict) -> dict[str, dict]:
    overview_data = row.get("overview_data", {}) if isinstance(row.get("overview_data", {}), dict) else {}
    lookup: dict[str, dict] = {}
    for item in list(overview_data.get("colors", [])):
        if not isinstance(item, dict):
            continue
        colorway = str(item.get("colorway", "")).strip()
        if colorway:
            lookup[colorway.upper()] = item
    return lookup

def build_tech_pack_bom_color_token_set(self, row: dict) -> set[str]:
    token_set = {"PANTONE", "TCX"}
    for color_meta in self.get_tech_pack_color_lookup(row).values():
        for field_name in ("color_name", "colorway"):
            raw_value = str(color_meta.get(field_name, "")).strip().upper()
            for token in re.findall(r"[A-Z0-9-]+", raw_value):
                if token:
                    token_set.add(token)
    return token_set

def resolve_tech_pack_bom_thumbnail_path(self, row: dict, section: dict) -> str:
    direct_path = str(section.get("thumbnail_path", "")).strip()
    if direct_path and resolve_image_source(direct_path) is not None:
        return direct_path
    style_code = str(row.get("style_code", "")).strip().upper()
    section_key = re.sub(r"[^A-Z0-9]+", "_", str(section.get("section", "")).strip().upper()).strip("_")
    if style_code and section_key:
        matches = sorted(DOWNLOAD_TECH_PACK_DATA_DIR.joinpath("previews").glob(f"{style_code}_bom_{section_key}_*.png"))
        if matches:
            return str(matches[0])
    return ""

def resolve_tech_pack_bom_item_thumbnail_path(self, row: dict, section: dict, item: dict, item_index: int) -> str:
    direct_path = str(item.get("thumbnail_path", "")).strip()
    if direct_path and resolve_image_source(direct_path) is not None:
        return direct_path
    item_count = len([entry for entry in list(section.get("items", [])) if isinstance(entry, dict)])
    if item_count > 1:
        return ""
    style_code = str(row.get("style_code", "")).strip().upper()
    section_key = re.sub(r"[^A-Z0-9]+", "_", str(section.get("section", "")).strip().upper()).strip("_")
    if style_code and section_key:
        matches = sorted(DOWNLOAD_TECH_PACK_DATA_DIR.joinpath("previews").glob(f"{style_code}_bom_{section_key}_*_item_{item_index}.png"))
        if matches:
            return str(matches[0])
    if item_count <= 1:
        return self.resolve_tech_pack_bom_thumbnail_path(row, section)
    return ""

def strip_tech_pack_bom_color_noise(self, value: str, color_tokens: set[str]) -> str:
    kept: list[str] = []
    for token in re.findall(r"[A-Z0-9%/-]+", str(value or "").upper()):
        if token in color_tokens:
            continue
        if re.fullmatch(r"\d{2}-\d{4}", token):
            continue
        if len(token) <= 2 and not token.endswith("%"):
            continue
        kept.append(token)
    return re.sub(r"\s+", " ", " ".join(kept)).strip()

def split_tech_pack_bom_repeated_prefix(self, value: str) -> tuple[str, str]:
    tokens = [token for token in str(value or "").strip().split() if token]
    if len(tokens) < 3:
        return "", " ".join(tokens)
    for size in range(len(tokens) // 2, 0, -1):
        prefix_tokens = tokens[:size]
        repeated_tokens = tokens[size : size * 2]
        remainder_tokens = tokens[size * 2 :]
        if prefix_tokens == repeated_tokens and remainder_tokens:
            return " ".join(prefix_tokens).strip(), " ".join(remainder_tokens).strip()
    return "", " ".join(tokens).strip()

def dedupe_repeated_tokens(self, value: str) -> str:
    tokens = [token for token in str(value or "").strip().split() if token]
    if len(tokens) == 2 and tokens[0] == tokens[1]:
        return tokens[0]
    if len(tokens) == 4 and tokens[0] == tokens[1] and tokens[2] == tokens[3]:
        return f"{tokens[0]} {tokens[2]}".strip()
    return " ".join(tokens).strip()

def build_tech_pack_artwork_colorways(self, item: dict, colorway_count: int) -> list[str]:
    article_code = self.dedupe_repeated_tokens(str(item.get("article_ref", "")).strip())
    composition_text = str(item.get("composition", "")).strip()
    composition_text = re.sub(r"\bPRODUCTION\b", "", composition_text, flags=re.IGNORECASE)
    composition_text = re.sub(r"\bARTWORK\b", "", composition_text, flags=re.IGNORECASE)
    composition_text = re.sub(r"\s+", " ", composition_text).strip()
    comp_tokens = [token for token in composition_text.split() if token]
    merged_suffix = composition_text
    if len(comp_tokens) >= 4 and comp_tokens[0] == comp_tokens[1] and comp_tokens[2] == comp_tokens[3]:
        merged_suffix = f"{comp_tokens[0]}{comp_tokens[2]}".strip()
    elif len(comp_tokens) == 2 and comp_tokens[0] == comp_tokens[1]:
        merged_suffix = comp_tokens[0]
    elif len(comp_tokens) == 1:
        merged_suffix = comp_tokens[0]
    merged_suffix = merged_suffix.strip()
    color_line = " ".join(part for part in (article_code, merged_suffix) if part).strip()
    if not color_line:
        return [""] * colorway_count
    return [color_line] * colorway_count

def get_tech_pack_bom_item_override(self, item: dict, field_name: str, default: str = "") -> str:
    overrides = item.get("display_overrides", {}) if isinstance(item.get("display_overrides", {}), dict) else {}
    value = overrides.get(field_name, default)
    return str(value or default).strip()

def get_tech_pack_bom_display_values(self, section_name: str, item: dict, color_tokens: set[str]) -> dict[str, str]:
    raw_component = str(item.get("component", "")).strip()
    raw_description = str(item.get("description", "")).strip()
    article = str(item.get("article_ref", "")).strip()
    composition = str(item.get("composition", "")).strip()
    comments = str(item.get("comments", "")).strip()
    section_upper = section_name.upper()

    component_display = raw_component or raw_description or article or composition
    description_display = raw_description or composition or article or raw_component
    article_display = article
    composition_display = composition

    if section_upper == "MATERIAL":
        cleaned_article = self.strip_tech_pack_bom_color_noise(article, color_tokens)
        cleaned_composition = self.strip_tech_pack_bom_color_noise(composition, color_tokens)
        material_name = cleaned_article or cleaned_composition
        material_name = re.sub(r"\bRAYON\b.*$", "", material_name).strip()
        material_name = re.sub(r"\bSPANDEX\b.*$", "", material_name).strip()
        material_name = material_name.rstrip("/").strip()
        if raw_component and material_name and raw_component.upper() not in material_name.upper():
            component_display = f"{raw_component} {material_name}".strip()
        else:
            component_display = material_name or raw_component or raw_description
        description_display = component_display
        article_display = ""
        composition_no_noise = cleaned_composition
        if material_name:
            composition_no_noise = re.sub(re.escape(material_name), "", composition_no_noise, flags=re.IGNORECASE).strip()
        composition_no_noise = re.sub(r"\s*/\s*", " / ", composition_no_noise)
        percentages = re.findall(r"\d+%\s*[A-Z]+", composition_no_noise.upper())
        composition_display = " / ".join(percentages) if percentages else composition_no_noise
    elif section_upper == "TRIM":
        cleaned_article = self.strip_tech_pack_bom_color_noise(article, color_tokens)
        cleaned_composition = self.strip_tech_pack_bom_color_noise(composition, color_tokens)
        component_display = " ".join(part for part in (raw_component, cleaned_article) if part).strip() or cleaned_article or raw_component or raw_description
        description_tail = cleaned_composition if cleaned_composition and cleaned_composition.upper() != raw_description.upper() else ""
        description_display = " ".join(part for part in (raw_description, description_tail) if part).strip() or component_display
        article_display = ""
        composition_display = ""
    elif section_upper == "LABELS":
        repeated_color, article_tail = self.split_tech_pack_bom_repeated_prefix(article)
        label_tail = article_tail or article
        label_text = " ".join(part for part in (raw_component, label_tail) if part).strip()
        component_display = label_text or raw_component or label_tail or raw_description
        description_display = component_display
        article_display = ""
        composition_display = ""
        if repeated_color and repeated_color.upper() == composition.upper():
            composition_display = ""
    elif section_upper == "ARTWORK":
        component_display = raw_component or raw_description or article
        description_display = raw_description or component_display
        if str(item.get("position", "")).strip().upper().endswith("ARTWORK") and "ARTWORK" not in description_display.upper():
            description_display = f"{description_display} ARTWORK".strip()
        article_display = ""
        composition_display = ""

    return {
        "component": self.get_tech_pack_bom_item_override(item, "component", component_display),
        "description": self.get_tech_pack_bom_item_override(item, "description", description_display),
        "comments": self.get_tech_pack_bom_item_override(item, "comments", comments),
        "article": self.get_tech_pack_bom_item_override(item, "article", article_display),
        "composition": self.get_tech_pack_bom_item_override(item, "composition", composition_display),
    }

def get_tech_pack_bom_color_secondary_line(self, item: dict, color_index: int) -> str:
    source_text = " ".join(
        str(item.get(key, "")).upper()
        for key in ("composition", "article_ref", "description", "component")
    )
    codes = re.findall(r"\d{2}-\d{4}", source_text)
    if 0 <= color_index < len(codes):
        return f"PANTONE {codes[color_index]} TCX"
    return ""

def get_tech_pack_bom_color_primary_line(self, section_name: str, item: dict, color_name: str, color_index: int) -> str:
    section_upper = str(section_name).upper()
    if section_upper == "LABELS":
        repeated_color, _ = self.split_tech_pack_bom_repeated_prefix(str(item.get("article_ref", "")).strip())
        if repeated_color:
            return repeated_color
    if section_upper == "ARTWORK":
        color_lines = self.build_tech_pack_artwork_colorways(item, max(1, color_index + 1))
        if 0 <= color_index < len(color_lines):
            return color_lines[color_index]
    return color_name

def get_tech_pack_bom_color_override(self, item: dict, colorway: str, field_name: str, default: str = "") -> str:
    overrides = item.get("colorway_overrides", {}) if isinstance(item.get("colorway_overrides", {}), dict) else {}
    color_override = overrides.get(str(colorway).strip().upper(), {})
    if isinstance(color_override, dict):
        return str(color_override.get(field_name, default) or default).strip()
    return str(default or "").strip()

def tech_pack_bom_item_has_colorway_data(self, item: dict) -> bool:
    text = " ".join(
        str(item.get(key, "")).upper()
        for key in ("article_ref", "composition", "description", "component")
    )
    return "PANTONE" in text or bool(re.search(r"\d{2}-\d{4}", text))

def build_tech_pack_bom_color_rows(self, *, primary: str, secondary: str, swatch_color: str, include_swatch: bool) -> list[dict[str, str]]:
    rows: list[dict[str, str]] = []
    if primary.strip():
        rows.append({"text": primary.strip(), "background": "#ffffff"})
    if secondary.strip():
        rows.append({"text": secondary.strip(), "background": "#ffffff"})
    if include_swatch:
        rows.append({"text": "", "background": swatch_color})
    return rows

def should_show_tech_pack_bom_item_image(self, section_name: str, item_index: int, item_count: int) -> bool:
    if str(section_name).upper() == "TRIM" and item_count > 1:
        return item_index == 1
    return True

def build_tech_pack_bom_detail_box(
    self,
    parent: tk.Widget,
    *,
    title: str,
    value: str,
    row_index: int,
    column_index: int,
    columnspan: int = 1,
    min_height: int = 24,
    wraplength: int = 220,
    detail_state: dict | None = None,
    field_key: str = "",
) -> None:
    box = ttk.Frame(parent)
    box.grid(row=row_index, column=column_index, columnspan=columnspan, sticky="nsew")
    box.columnconfigure(0, weight=1)
    tk.Label(box, text=f"{title}:", font=("Segoe UI", 8, "bold"), bg="#ffffff", anchor="w").grid(
        row=0, column=0, sticky="ew", padx=(1, 1), pady=(1, 0)
    )
    if detail_state is not None and field_key:
        text_widget = tk.Text(
            box,
            wrap="word",
            height=1 if min_height <= 40 else 2,
            relief="solid",
            bd=1,
            font=("Segoe UI", 8),
            bg="#ffffff",
            fg="#111111",
            padx=4,
            pady=3,
        )
        text_widget.grid(row=1, column=0, sticky="nsew", padx=(1, 1), pady=(0, 1), ipady=max(0, min_height - 24))
        text_widget.insert("1.0", value)
        text_widget.configure(state="normal" if detail_state.get("edit_mode") else "disabled")
        detail_state.setdefault("text_entries", []).append((text_widget, False))
        detail_state.setdefault("bom_widgets", {})[field_key] = text_widget
    else:
        tk.Label(
            box,
            text=value,
            font=("Segoe UI", 8),
            bg="#ffffff",
            fg="#111111",
            relief="solid",
            bd=1,
            anchor="nw",
            justify="left",
            padx=4,
            pady=3,
            wraplength=wraplength,
        ).grid(row=1, column=0, sticky="nsew", padx=(1, 1), pady=(0, 1), ipady=max(0, min_height - 24))

def build_tech_pack_bom_color_box(
    self,
    parent: tk.Widget,
    *,
    value: str,
    row_index: int,
    min_height: int = 40,
    wraplength: int = 220,
    background: str = "#ffffff",
    detail_state: dict | None = None,
    field_key: str = "",
) -> None:
    if detail_state is not None and field_key and background == "#ffffff":
        text_widget = tk.Text(
            parent,
            wrap="word",
            height=1,
            relief="solid",
            bd=1,
            font=("Segoe UI", 9),
            bg=background,
            fg="#111111",
            padx=6,
            pady=4,
        )
        text_widget.grid(row=row_index, column=0, sticky="nsew", padx=(1, 1), pady=(0, 1), ipady=max(0, min_height - 24))
        text_widget.insert("1.0", value)
        text_widget.configure(state="normal" if detail_state.get("edit_mode") else "disabled")
        detail_state.setdefault("text_entries", []).append((text_widget, False))
        detail_state.setdefault("bom_widgets", {})[field_key] = text_widget
        return
    tk.Label(
        parent,
        text=value,
        font=("Segoe UI", 9),
        bg=background,
        fg="#111111",
        relief="solid",
        bd=1,
        anchor="nw",
        justify="left",
        padx=6,
        pady=4,
        wraplength=wraplength,
    ).grid(row=row_index, column=0, sticky="nsew", padx=(1, 1), pady=(0, 1), ipady=max(0, min_height - 24))

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

def build_tech_pack_bom_tab(self, parent: ttk.Frame, row: dict, detail_state: dict | None = None) -> None:
    try:
        self.repair_tech_pack_bom_assets(row, persist=False)
    except Exception:
        pass
    parent.columnconfigure(0, weight=1)
    parent.rowconfigure(0, weight=1)

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

    canvas = tk.Canvas(scroll_host, highlightthickness=0, bg="#ffffff")
    canvas.grid(row=0, column=0, sticky="nsew")
    scrollbar = ttk.Scrollbar(scroll_host, orient="vertical", command=canvas.yview)
    scrollbar.grid(row=0, column=1, sticky="ns")
    canvas.configure(yscrollcommand=scrollbar.set)

    body = ttk.Frame(canvas)
    window_id = canvas.create_window((0, 0), window=body, anchor="nw")

    def sync_width(event: tk.Event) -> None:
        canvas.itemconfigure(window_id, width=event.width)

    def update_scrollregion(_event: tk.Event) -> None:
        canvas.configure(scrollregion=canvas.bbox("all"))

    def on_mousewheel(event: tk.Event) -> str:
        canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
        return "break"

    def bind_mousewheel(_event: tk.Event) -> None:
        canvas.bind_all("<MouseWheel>", on_mousewheel)

    def unbind_mousewheel(_event: tk.Event) -> None:
        canvas.unbind_all("<MouseWheel>")

    canvas.bind("<Configure>", sync_width)
    body.bind("<Configure>", update_scrollregion)
    for widget in (canvas, body, parent):
        widget.bind("<Enter>", bind_mousewheel)
        widget.bind("<Leave>", unbind_mousewheel)

    sections = [item for item in list(row.get("bom_data", [])) if isinstance(item, dict)]
    color_lookup = self.get_tech_pack_color_lookup(row)
    color_tokens = self.build_tech_pack_bom_color_token_set(row)

    if not sections:
        ttk.Label(body, text=self.display_value(row.get("bom_summary"))).grid(row=0, column=0, sticky="w")
        return
    body.columnconfigure(0, weight=1)

    ordered_sections: list[dict] = []
    section_map = {str(section.get("section", "")).strip().upper(): section for section in sections}
    for name in ("MATERIAL", "TRIM", "LABELS", "PACKAGING", "ARTWORK"):
        if name in section_map:
            ordered_sections.append(section_map[name])
    for section in sections:
        if section not in ordered_sections:
            ordered_sections.append(section)

    for section_index, section in enumerate(ordered_sections):
        section_name = str(section.get("section", "Section")).strip() or "Section"
        items = [item for item in list(section.get("items", [])) if isinstance(item, dict)]
        colorways = [str(item).strip() for item in list(section.get("colorways", [])) if str(item).strip()]

        section_frame = ttk.Frame(body)
        section_frame.grid(row=section_index, column=0, sticky="ew", padx=8, pady=(8 if section_index == 0 else 0, 12))
        section_frame.columnconfigure(0, weight=62, minsize=980)
        for color_idx in range(len(colorways)):
            section_frame.columnconfigure(color_idx + 1, weight=19, minsize=320)

        tk.Label(
            section_frame,
            text=section_name,
            bg="#d9d9d9",
            fg="#222222",
            font=("Segoe UI", 10, "bold"),
            relief="solid",
            bd=1,
            anchor="w",
            padx=6,
            pady=5,
        ).grid(row=0, column=0, sticky="ew")

        if colorways:
            tk.Label(
                section_frame,
                text="Colorways",
                bg="#d9d9d9",
                fg="#222222",
                font=("Segoe UI", 10, "bold"),
                relief="solid",
                bd=1,
                anchor="center",
                padx=6,
                pady=5,
            ).grid(row=0, column=1, columnspan=max(1, len(colorways)), sticky="ew")

        for item_index, item in enumerate(items, start=1):
            display_values = self.get_tech_pack_bom_display_values(section_name, item, color_tokens)
            position_value = str(item.get("position", "")).strip()
            if "Position:" in position_value:
                position_value = position_value.split("Position:", 1)[-1].strip()
            section_type = "KNIT" if section_name.upper() == "MATERIAL" else section_name.rstrip("S")
            qty_raw = str(item.get("qty_um", "")).strip()
            qty_parts = qty_raw.split()
            qty_value = qty_parts[0] if qty_parts else ""
            qty_suffix = " ".join(qty_parts[1:]) if len(qty_parts) > 1 else ""
            if section_name.upper() in {"TRIM", "LABELS"} and qty_suffix and qty_suffix.upper() not in position_value.upper():
                position_value = " ".join(part for part in (position_value, qty_suffix) if part).strip()
            if section_name.upper() == "MATERIAL":
                supplier_value = " ".join(
                    part for part in (str(item.get("supplier", "")).strip(), str(item.get("trim_size_um", "")).strip()) if part
                ).strip()
                trim_size_value = ""
            elif section_name.upper() == "LABELS":
                supplier_value = " ".join(
                    part for part in (str(item.get("supplier", "")).strip(), str(item.get("trim_size_um", "")).strip()) if part
                ).strip()
                trim_size_value = ""
            else:
                supplier_value = str(item.get("supplier", "")).strip()
                trim_size_value = str(item.get("trim_size_um", "")).strip()
            position_value = self.get_tech_pack_bom_item_override(item, "position", position_value)
            qty_value = self.get_tech_pack_bom_item_override(item, "qty_um", qty_value)
            section_type = self.get_tech_pack_bom_item_override(item, "type", section_type)
            supplier_value = self.get_tech_pack_bom_item_override(item, "supplier", supplier_value)
            trim_size_value = self.get_tech_pack_bom_item_override(item, "trim_size_um", trim_size_value)
            has_colorway_data = self.tech_pack_bom_item_has_colorway_data(item)

            detail_grid = ttk.Frame(section_frame)
            detail_grid.grid(row=item_index, column=0, sticky="nsew")
            detail_grid.columnconfigure(0, weight=0, minsize=210)
            detail_grid.columnconfigure(1, weight=30, minsize=210, uniform=f"bom_detail_{section_index}")
            detail_grid.columnconfigure(2, weight=30, minsize=210, uniform=f"bom_detail_{section_index}")
            detail_grid.columnconfigure(3, weight=28, minsize=190, uniform=f"bom_detail_{section_index}")
            detail_grid.rowconfigure(0, minsize=40)
            detail_grid.rowconfigure(1, minsize=40)
            detail_grid.rowconfigure(2, minsize=40)
            detail_grid.rowconfigure(3, minsize=40)

            preview_photo = None
            item_thumbnail_path = self.resolve_tech_pack_bom_item_thumbnail_path(row, section, item, item_index)
            if item_thumbnail_path or self.should_show_tech_pack_bom_item_image(section_name, item_index, len(items)):
                preview_row = dict(row)
                preview_row["image_path"] = item_thumbnail_path
                preview_photo = self.create_tech_pack_bom_preview_photo(preview_row["image_path"], target_width=118, target_height=126)
            preview_label = tk.Label(detail_grid, bg="#ffffff", relief="solid", bd=1)
            preview_label.grid(row=0, column=0, rowspan=4, sticky="nsew", padx=(1, 1), pady=(1, 1))
            if preview_photo is not None:
                preview_label.configure(image=preview_photo)
                preview_label.image = preview_photo
            else:
                preview_label.configure(text="", font=("Segoe UI", 9))

            self.build_tech_pack_bom_detail_box(
                detail_grid,
                title="Component",
                value=display_values["component"],
                row_index=0,
                column_index=1,
                wraplength=220,
                detail_state=detail_state,
                field_key=f"bom::{section_index}::{item_index - 1}::display::component",
            )
            self.build_tech_pack_bom_detail_box(
                detail_grid,
                title="Article #",
                value=display_values["article"],
                row_index=0,
                column_index=2,
                columnspan=2,
                wraplength=360,
                detail_state=detail_state,
                field_key=f"bom::{section_index}::{item_index - 1}::display::article",
            )
            self.build_tech_pack_bom_detail_box(
                detail_grid,
                title="Description",
                value=display_values["description"],
                row_index=1,
                column_index=1,
                wraplength=220,
                detail_state=detail_state,
                field_key=f"bom::{section_index}::{item_index - 1}::display::description",
            )
            self.build_tech_pack_bom_detail_box(
                detail_grid,
                title="Composition",
                value=display_values["composition"],
                row_index=1,
                column_index=2,
                columnspan=2,
                wraplength=360,
                detail_state=detail_state,
                field_key=f"bom::{section_index}::{item_index - 1}::display::composition",
            )
            self.build_tech_pack_bom_detail_box(
                detail_grid,
                title="Position",
                value=position_value,
                row_index=2,
                column_index=1,
                wraplength=220,
                detail_state=detail_state,
                field_key=f"bom::{section_index}::{item_index - 1}::display::position",
            )
            self.build_tech_pack_bom_detail_box(
                detail_grid,
                title="Qty/UM",
                value=qty_value,
                row_index=2,
                column_index=2,
                wraplength=140,
                detail_state=detail_state,
                field_key=f"bom::{section_index}::{item_index - 1}::display::qty_um",
            )
            self.build_tech_pack_bom_detail_box(
                detail_grid,
                title="Type",
                value=section_type,
                row_index=2,
                column_index=3,
                wraplength=140,
                detail_state=detail_state,
                field_key=f"bom::{section_index}::{item_index - 1}::display::type",
            )
            self.build_tech_pack_bom_detail_box(
                detail_grid,
                title="Supplier",
                value=supplier_value,
                row_index=3,
                column_index=1,
                wraplength=220,
                detail_state=detail_state,
                field_key=f"bom::{section_index}::{item_index - 1}::display::supplier",
            )
            self.build_tech_pack_bom_detail_box(
                detail_grid,
                title="Trim Size/UM",
                value=trim_size_value,
                row_index=3,
                column_index=2,
                wraplength=220,
                detail_state=detail_state,
                field_key=f"bom::{section_index}::{item_index - 1}::display::trim_size_um",
            )
            self.build_tech_pack_bom_detail_box(
                detail_grid,
                title="Comments",
                value=display_values["comments"],
                row_index=4,
                column_index=0,
                columnspan=4,
                min_height=40,
                wraplength=820,
                detail_state=detail_state,
                field_key=f"bom::{section_index}::{item_index - 1}::display::comments",
            )

            for color_offset, colorway in enumerate(colorways, start=1):
                color_meta = color_lookup.get(colorway.upper(), {})
                default_color_name = str(color_meta.get("color_name", "")).strip() if has_colorway_data else ""
                color_name = self.get_tech_pack_bom_color_primary_line(section_name, item, default_color_name, color_offset - 1)
                color_secondary = self.get_tech_pack_bom_color_secondary_line(item, color_offset - 1) if has_colorway_data else ""
                color_name = self.get_tech_pack_bom_color_override(item, colorway, "primary", color_name)
                color_secondary = self.get_tech_pack_bom_color_override(item, colorway, "secondary", color_secondary)
                swatch_color = self.tech_pack_preview_swatch_color(color_name, colorway) if has_colorway_data else "#ffffff"
                color_rows = self.build_tech_pack_bom_color_rows(
                    primary=color_name,
                    secondary=color_secondary,
                    swatch_color=swatch_color,
                    include_swatch=has_colorway_data,
                )
                color_frame = ttk.Frame(section_frame)
                color_frame.grid(row=item_index, column=color_offset, sticky="nsew")
                color_frame.columnconfigure(0, weight=1, minsize=300)
                tk.Label(
                    color_frame,
                    text=colorway,
                    bg="#d9d9d9",
                    fg="#222222",
                    font=("Segoe UI", 9, "bold"),
                    relief="solid",
                    bd=1,
                    anchor="w",
                    padx=6,
                    pady=4,
                ).grid(row=0, column=0, sticky="ew", padx=(1, 1), pady=(1, 0))
                for row_number, row_data in enumerate(color_rows, start=1):
                    color_frame.rowconfigure(row_number, minsize=40)
                    self.build_tech_pack_bom_color_box(
                        color_frame,
                        value=row_data["text"],
                        row_index=row_number,
                        min_height=40,
                        wraplength=250,
                        background=row_data["background"],
                        detail_state=detail_state,
                        field_key=(
                            f"bom::{section_index}::{item_index - 1}::color::{colorway.upper()}::"
                            f"{'primary' if row_number == 1 else 'secondary'}"
                            if row_data["background"] == "#ffffff" and row_number <= 2
                            else ""
                        ),
                    )

def open_tech_pack_preview_gallery(self, image_paths: list[str], start_index: int = 0) -> None:
    valid_paths = [resolved for path in image_paths if (resolved := resolve_image_source(path)) is not None]
    if not valid_paths:
        return
    dialog = tk.Toplevel(self.root)
    dialog.title("Tech Pack Images")
    dialog.resizable(True, True)
    dialog.minsize(700, 600)
    dialog.geometry("980x760")

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

    header = ttk.Frame(frame)
    header.grid(row=0, column=0, sticky="ew", pady=(0, 8))
    header.columnconfigure(1, weight=1)
    index_var = tk.StringVar()
    ttk.Button(header, text="<", command=lambda: show_image(current["index"] - 1)).grid(row=0, column=0, padx=(0, 8))
    ttk.Label(header, textvariable=index_var, anchor="center").grid(row=0, column=1, sticky="ew")
    ttk.Button(header, text=">", command=lambda: show_image(current["index"] + 1)).grid(row=0, column=2)

    canvas = tk.Canvas(frame, bg="#ffffff", highlightthickness=0)
    canvas.grid(row=1, column=0, sticky="nsew")
    image_label = tk.Label(canvas, bg="#ffffff")
    canvas_window = canvas.create_window((0, 0), window=image_label, anchor="center")
    current: dict[str, int] = {"index": max(0, min(start_index, len(valid_paths) - 1))}

    def show_image(index: int) -> None:
        current["index"] = index % len(valid_paths)
        image_path = valid_paths[current["index"]]
        with Image.open(image_path).convert("RGB") as image:
            max_width = max(320, canvas.winfo_width() - 24)
            max_height = max(240, canvas.winfo_height() - 24)
            scale = min(max_width / image.width, max_height / image.height, 1.0)
            resized = image.resize(
                (max(1, int(image.width * scale)), max(1, int(image.height * scale))),
                Image.Resampling.LANCZOS,
            )
            photo = ImageTk.PhotoImage(resized)
        image_label.configure(image=photo)
        image_label.image = photo
        index_var.set(f"{current['index'] + 1} / {len(valid_paths)}")
        canvas.coords(canvas_window, canvas.winfo_width() / 2, canvas.winfo_height() / 2)

    canvas.bind("<Configure>", lambda _event: show_image(current["index"]))
    show_image(current["index"])

def toggle_tech_pack_detail_edit_mode(self, detail_state: dict, enabled: bool) -> None:
    if enabled and not self.is_action_allowed("action.tech_pack.detail_edit"):
        return
    detail_state["edit_mode"] = enabled
    for entry, is_readonly in detail_state.get("field_entries", []):
        if is_readonly:
            entry.configure(state="readonly")
        else:
            entry.configure(state="normal" if enabled else "readonly")
    for text_widget, is_readonly in detail_state.get("text_entries", []):
        if is_readonly:
            text_widget.configure(state="disabled")
        else:
            text_widget.configure(state="normal" if enabled else "disabled")
    for combo in detail_state.get("consumption_combo_widgets", []):
        try:
            combo.configure(state="readonly" if enabled else "disabled")
        except Exception:
            continue
    for button in detail_state.get("consumption_buttons", []):
        try:
            button.configure(state="normal" if enabled else "disabled")
        except Exception:
            continue


def get_current_tech_pack_detail_tab_key(self, detail_state: dict) -> str:
    notebook = detail_state.get("notebook")
    tab_ids = detail_state.get("tab_ids", {})
    if notebook is None:
        return "overview"
    try:
        selected_tab_id = str(notebook.select())
    except Exception:
        return "overview"
    return str(tab_ids.get(selected_tab_id, "overview")).strip() or "overview"


def build_tech_pack_detail_patch(self, row: dict, detail_state: dict) -> tuple[str, dict]:
    style_code = str(row.get("style_code", "")).strip()
    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()
    }
    record = dict(library_by_style.get(style_code, self.default_tech_pack_record(style_code)))
    overview_data = dict(record.get("overview_data", {})) if isinstance(record.get("overview_data", {}), dict) else {}
    detail_tab_overrides = dict(record.get("detail_tab_overrides", {})) if isinstance(record.get("detail_tab_overrides", {}), dict) else {}
    current_tab = self.get_current_tech_pack_detail_tab_key(detail_state)
    changes: dict[str, object] = {
        "last_updated": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
    }

    if current_tab == "overview":
        field_vars = detail_state.get("field_vars", {})
        selected_cover_image = str(detail_state.get("cover_var").get() if detail_state.get("cover_var") else "").strip()
        for key, variable in field_vars.items():
            value = variable.get().strip()
            if key == "lead_style_no":
                normalized_value = normalize_style_identifier(value)
                if value.lower() == "lead" or normalized_value == style_code:
                    value = "Lead"
                elif normalized_value:
                    value = normalized_value
            if key == "style_name":
                changes["style_name"] = value
            elif key == "season":
                if hasattr(self, "split_season_display"):
                    value, parsed_year = self.split_season_display(value, overview_data.get("season_year", ""))
                    if parsed_year:
                        changes["season_year"] = parsed_year
                        overview_data["season_year"] = parsed_year
                changes["season"] = value
                overview_data["season"] = value
            elif key == "season_year":
                if not value and str(changes.get("season_year", "")).strip():
                    value = str(changes.get("season_year", "")).strip()
                changes["season_year"] = self.normalize_season_year(value) if hasattr(self, "normalize_season_year") else value
                overview_data["season_year"] = changes["season_year"]
            elif key == "brand":
                changes["collection"] = value
                overview_data["brand"] = value
            elif key == "status":
                changes["production_status"] = value
                overview_data["status"] = value
            elif key == "style_code":
                continue
            else:
                overview_data[key] = value
        season_value = str(overview_data.get("season", "")).strip() or str(record.get("season", "")).strip()
        season_year_value = str(overview_data.get("season_year", "")).strip() or str(record.get("season_year", "")).strip()
        if hasattr(self, "build_season_display"):
            changes["season_display"] = self.build_season_display(season_value, season_year_value)
        if hasattr(self, "build_style_identity"):
            changes["style_identity"] = self.build_style_identity(style_code, season_value, season_year_value)
        for key, widget in detail_state.get("text_widgets", {}).items():
            value = widget.get("1.0", "end").strip()
            if key == "design_details":
                changes["overview_notes"] = value
                changes["instructions_notes"] = value
                overview_data["design_details"] = value
            else:
                overview_data[key] = value
        changes["overview_data"] = overview_data
        if selected_cover_image:
            preview_images = [
                str(item).strip()
                for item in list(detail_state.get("preview_images", []) or record.get("preview_images", []))
                if str(item).strip()
            ]
            reordered_preview_images = [selected_cover_image]
            reordered_preview_images.extend(
                image_path
                for image_path in preview_images
                if image_path and image_path != selected_cover_image
            )
            changes["preview_images"] = reordered_preview_images
            changes["image_path"] = selected_cover_image
        return current_tab, changes

    if current_tab == "bom":
        bom_data = deepcopy(record.get("bom_data") if isinstance(record.get("bom_data"), list) else row.get("bom_data", []))
        if not isinstance(bom_data, list):
            bom_data = []
        for field_key, widget in detail_state.get("bom_widgets", {}).items():
            try:
                _prefix, section_index_text, item_index_text, field_group, *rest = field_key.split("::")
                section_index = int(section_index_text)
                item_index = int(item_index_text)
                item = bom_data[section_index]["items"][item_index]
            except Exception:
                continue
            value = widget.get("1.0", "end").strip()
            if field_group == "display" and rest:
                item.setdefault("display_overrides", {})[rest[0]] = value
            elif field_group == "color" and len(rest) >= 2:
                colorway_key = rest[0]
                field_name = rest[1]
                item.setdefault("colorway_overrides", {}).setdefault(colorway_key, {})[field_name] = value
        changes["bom_data"] = bom_data
        return current_tab, changes

    if current_tab == "consumption":
        serialized_versions: list[dict] = []
        for version_state in list(detail_state.get("consumption_versions_state", [])):
            if not isinstance(version_state, dict):
                continue
            serialized_lines: list[dict] = []
            for line_state in list(version_state.get("lines", [])):
                if not isinstance(line_state, dict):
                    continue
                serialized_lines.append(
                    {
                        "line_id": str(line_state.get("line_id", "")).strip() or uuid.uuid4().hex,
                        "costing_id": str(line_state.get("costing_id", "")).strip(),
                        "bom_item_id": str(line_state.get("bom_item_id", "")).strip(),
                        "bom_item_code": str(line_state.get("bom_item_code", "")).strip(),
                        "bom_item_name": str(line_state.get("bom_item_name", "")).strip(),
                        "fabric_type": str(line_state.get("fabric_type", "")).strip(),
                        "section": str(line_state.get("section", "")).strip(),
                        "color": str(line_state.get("color", "")).strip(),
                        "trim_size_um": str(line_state.get("trim_size_um", "")).strip(),
                        "length_value": str(line_state.get("length_value", "")).strip(),
                        "width_value": str(line_state.get("width_value", "")).strip(),
                        "gsm_value": str(line_state.get("gsm_value", "")).strip(),
                        "waste_percent": str(line_state.get("waste_percent", "")).strip(),
                        "fabric_weight": str(line_state.get("fabric_weight", "")).strip(),
                        "fabric_weight_uom": str(line_state.get("fabric_weight_uom", "")).strip(),
                        "uom": str(
                            line_state.get("uom_var").get()
                            if line_state.get("uom_var") is not None
                            else line_state.get("uom", "")
                        ).strip(),
                        "consumption": str(line_state.get("consumption_var").get() if line_state.get("consumption_var") else "").strip(),
                    }
                )
            serialized_versions.append(
                {
                    "version_id": str(version_state.get("version_id", "")).strip() or str(version_state.get("version_type", "")).strip(),
                    "version_type": str(version_state.get("version_type", "")).strip(),
                    "version_name": str(version_state.get("version_name", "")).strip(),
                    "lines": serialized_lines,
                }
            )
        changes["consumption_versions"] = serialized_versions
        return current_tab, changes

    structured_widgets = detail_state.get("structured_widgets", {})
    if current_tab == "constructions":
        construction_data = deepcopy(record.get("construction_data") if isinstance(record.get("construction_data"), list) else row.get("construction_data", []))
        for field_key, widget in structured_widgets.items():
            if not str(field_key).startswith("construction::"):
                continue
            value = widget.get("1.0", "end").strip()
            try:
                _group, index_text, field_name = field_key.split("::", 2)
                index = int(index_text)
            except ValueError:
                continue
            if 0 <= index < len(construction_data):
                construction_data[index][field_name] = value
        changes["construction_data"] = construction_data
        return current_tab, changes

    if current_tab == "instructions":
        design_intent_data = deepcopy(record.get("design_intent_data") if isinstance(record.get("design_intent_data"), list) else row.get("design_intent_data", []))
        attachments_data = deepcopy(record.get("attachments_data") if isinstance(record.get("attachments_data"), list) else row.get("attachments_data", []))
        instructions_notes = str(record.get("instructions_notes", "")).strip()
        for field_key, widget in structured_widgets.items():
            value = widget.get("1.0", "end").strip()
            try:
                group, index_text, field_name = field_key.split("::", 2)
                index = int(index_text)
            except ValueError:
                continue
            if group == "intent" and 0 <= index < len(design_intent_data):
                design_intent_data[index][field_name] = value
            elif group == "attachment" and 0 <= index < len(attachments_data):
                attachments_data[index][field_name] = int(value) if field_name == "page_number" and value.isdigit() else value
            elif group == "record" and field_name == "instructions_notes":
                instructions_notes = value
        changes["design_intent_data"] = design_intent_data
        changes["attachments_data"] = attachments_data
        changes["instructions_notes"] = instructions_notes
        return current_tab, changes

    if current_tab == "stitching":
        stitching_data = deepcopy(record.get("stitching_data") if isinstance(record.get("stitching_data"), list) else row.get("stitching_data", []))
        for field_key, widget in structured_widgets.items():
            if not str(field_key).startswith("stitching::"):
                continue
            value = widget.get("1.0", "end").strip()
            try:
                _group, index_text, field_name = field_key.split("::", 2)
                index = int(index_text)
            except ValueError:
                continue
            if 0 <= index < len(stitching_data):
                stitching_data[index][field_name] = value
        changes["stitching_data"] = stitching_data
        return current_tab, changes

    if current_tab == "pom":
        pom_data = deepcopy(record.get("pom_data") if isinstance(record.get("pom_data"), list) else row.get("pom_data", []))
        pending_pom_translations: dict[str, dict[str, str]] = {}
        for field_key, widget in structured_widgets.items():
            value = widget.get("1.0", "end").strip()
            try:
                group, index_text, field_name = field_key.split("::", 2)
                index = int(index_text)
            except ValueError:
                continue
            if group == "pom" and 0 <= index < len(pom_data):
                if field_name == "pages":
                    pom_data[index][field_name] = [int(part.strip()) for part in value.split(",") if part.strip().isdigit()]
                elif field_name == "raw_text":
                    pom_data[index][field_name] = [part.strip() for part in value.split("\n\n") if part.strip()]
                else:
                    pom_data[index][field_name] = value
            elif group == "pomrow" and 0 <= index < len(pom_data):
                try:
                    row_index_text, row_field = field_name.split("::", 1)
                    row_index = int(row_index_text)
                except ValueError:
                    continue
                table_rows = pom_data[index].get("table_rows")
                if not isinstance(table_rows, list):
                    table_rows = self.extract_tech_pack_pom_rows(pom_data[index])
                    pom_data[index]["table_rows"] = table_rows
                if not (0 <= row_index < len(table_rows)):
                    continue
                row_entry = table_rows[row_index]
                if not isinstance(row_entry, dict):
                    continue
                if row_field == "translate":
                    continue
                if row_field.startswith("size_"):
                    try:
                        size_index = int(row_field.split("_", 1)[1])
                    except ValueError:
                        continue
                    size_values = row_entry.get("size_values")
                    if not isinstance(size_values, list):
                        size_values = []
                    while len(size_values) <= size_index:
                        size_values.append("")
                    size_values[size_index] = value
                    row_entry["size_values"] = size_values
                else:
                    row_entry[row_field] = value
            elif group == "pomsub" and 0 <= index < len(pom_data):
                try:
                    row_index_text, row_field = field_name.split("::", 1)
                    row_index = int(row_index_text)
                except ValueError:
                    continue
                table_rows = pom_data[index].get("table_rows")
                if not isinstance(table_rows, list):
                    table_rows = self.extract_tech_pack_pom_rows(pom_data[index])
                    pom_data[index]["table_rows"] = table_rows
                if not (0 <= row_index < len(table_rows)):
                    continue
                row_entry = table_rows[row_index]
                if not isinstance(row_entry, dict) or not row_field.startswith("ps_"):
                    continue
                try:
                    ps_index = int(row_field.split("_", 1)[1])
                except ValueError:
                    continue
                ps_values = row_entry.get("ps_values")
                if not isinstance(ps_values, list):
                    ps_values = []
                while len(ps_values) <= ps_index:
                    ps_values.append("")
                ps_values[ps_index] = value
                row_entry["ps_values"] = ps_values
        for field_key, widget in structured_widgets.items():
            if not str(field_key).startswith("pomrow::"):
                continue
            try:
                group, index_text, field_name = field_key.split("::", 2)
                index = int(index_text)
                row_index_text, row_field = field_name.split("::", 1)
                row_index = int(row_index_text)
            except ValueError:
                continue
            if row_field != "translate" or not (0 <= index < len(pom_data)):
                continue
            description_widget = structured_widgets.get(f"pomrow::{index}::{row_index}::description")
            description_value = ""
            if description_widget is not None:
                try:
                    description_value = description_widget.get("1.0", "end").strip()
                except Exception:
                    description_value = ""
            if not description_value:
                table_rows = pom_data[index].get("table_rows")
                if isinstance(table_rows, list) and 0 <= row_index < len(table_rows) and isinstance(table_rows[row_index], dict):
                    description_value = str(table_rows[row_index].get("description", "")).strip()
            translate_value = widget.get("1.0", "end").strip()
            translation_key = self.normalize_tech_pack_pom_translation_key(description_value)
            if translation_key and translate_value:
                pending_pom_translations[translation_key] = {"vi": translate_value}
        for item in pom_data:
            if not isinstance(item, dict):
                continue
            table_rows = item.get("table_rows")
            if isinstance(table_rows, list) and table_rows:
                item.pop("raw_text", None)
        detail_state["pending_pom_translations"] = pending_pom_translations
        changes["pom_data"] = pom_data
        return current_tab, changes

    if current_tab == "attachments":
        return current_tab, {}

    if current_tab == "more":
        for field_key, widget in structured_widgets.items():
            value = widget.get("1.0", "end").strip()
            if field_key == "record::0::production_status":
                changes["production_status"] = value
            elif field_key == "record::0::quality_status":
                changes["quality_status"] = value
            elif field_key == "record::1::overview_notes" or field_key == "record::0::overview_notes":
                changes["overview_notes"] = value
        return current_tab, changes

    return current_tab, changes

def save_tech_pack_detail_overview(self, row: dict, detail_state: dict) -> None:
    if not self.is_action_allowed("action.tech_pack.detail_save"):
        return
    style_code = str(row.get("style_code", "")).strip()
    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()
    }
    try:
        current_tab, changes = self.build_tech_pack_detail_patch(row, detail_state)
    except Exception as exc:
        if hasattr(self, "append_tech_pack_save_debug_log"):
            try:
                self.append_tech_pack_save_debug_log(
                    "detail-save build-patch-error "
                    + json.dumps(
                        {
                            "style_code": style_code,
                            "message": str(exc),
                            "traceback": traceback.format_exc(limit=10),
                            "edit_mode": bool(detail_state.get("edit_mode")),
                        },
                        ensure_ascii=False,
                    )
                )
            except Exception:
                pass
        messagebox.showerror(self.t("warning_title"), str(exc))
        self.set_status(str(exc))
        return
    if hasattr(self, "append_tech_pack_save_debug_log"):
        try:
            self.append_tech_pack_save_debug_log(
                "detail-save context "
                + json.dumps(
                    {
                        "style_code": style_code,
                        "tab": current_tab,
                        "edit_mode": bool(detail_state.get("edit_mode")),
                        "field_entries": len(list(detail_state.get("field_entries", []))),
                        "text_entries": len(list(detail_state.get("text_entries", []))),
                        "structured_widgets": len(list(detail_state.get("structured_widgets", {}))),
                        "bom_widgets": len(list(detail_state.get("bom_widgets", {}))),
                        "consumption_versions": len(list(detail_state.get("consumption_versions_state", []))),
                    },
                    ensure_ascii=False,
                )
            )
        except Exception:
            pass
    relationship = self.get_tech_pack_relationship_context(row)
    if relationship["is_baby"] and current_tab in {"constructions", "instructions", "stitching", "pom"}:
        message = f"This tab is inherited from lead style {relationship['parent_style_code']}. Please edit the lead style instead."
        self.set_status(message)
        messagebox.showinfo(self.t("warning_title"), message)
        return
    if current_tab == "consumption":
        for version_state in list(detail_state.get("consumption_versions_state", [])):
            if not isinstance(version_state, dict):
                continue
            for line_state in list(version_state.get("lines", [])):
                if not isinstance(line_state, dict):
                    continue
                if not str(line_state.get("bom_item_code", "")).strip():
                    message = "Every consumption line must have BOM Item Code."
                    if hasattr(self, "append_tech_pack_save_debug_log"):
                        try:
                            self.append_tech_pack_save_debug_log(
                                "detail-save validation-error "
                                + json.dumps(
                                    {
                                        "style_code": style_code,
                                        "tab": current_tab,
                                        "version_type": str(version_state.get("version_type", "")).strip(),
                                        "line_state": {
                                            "line_id": str(line_state.get("line_id", "")).strip(),
                                            "costing_id": str(line_state.get("costing_id", "")).strip(),
                                            "bom_item_id": str(line_state.get("bom_item_id", "")).strip(),
                                            "bom_item_code": str(line_state.get("bom_item_code", "")).strip(),
                                        },
                                    },
                                    ensure_ascii=False,
                                )
                            )
                        except Exception:
                            pass
                    self.set_status(message)
                    messagebox.showwarning(self.t("warning_title"), message)
                    return
    if hasattr(self, "append_tech_pack_save_debug_log") and hasattr(self, "build_tech_pack_debug_summary"):
        try:
            summary = self.build_tech_pack_debug_summary(changes)
            self.append_tech_pack_save_debug_log(
                f"detail-save start style={style_code} tab={current_tab} summary={json.dumps(summary, ensure_ascii=False)}"
            )
        except Exception:
            pass
    if current_tab in {"samples", "attachments"}:
        tab_label = "Samples" if current_tab == "samples" else "Attachments"
        self.set_status(f"{tab_label} tab is read-only in Tech Pack Detail.")
        return
    pending_pom_translations = detail_state.get("pending_pom_translations", {}) if current_tab == "pom" else {}
    if bool(detail_state.get("save_running")):
        return
    detail_state["save_running"] = True
    save_button = detail_state.get("save_button")
    if save_button is not None:
        try:
            save_button.configure(state="disabled")
        except Exception:
            pass
    self.set_status(f"Saving Tech Pack {style_code}...")

    def worker() -> None:
        payload: dict[str, object]
        try:
            saved_record = self.save_tech_pack_record_patch_to_source(style_code, changes)
            synced_count = 0
            if current_tab == "consumption":
                synced_count = self.sync_bom_purchase_uom_from_consumption_versions(
                    changes.get("consumption_versions", []) if isinstance(changes.get("consumption_versions", []), list) else []
                )
                if hasattr(self, "sync_costing_form_materials_from_consumption"):
                    self.sync_costing_form_materials_from_consumption(
                        style_code,
                        changes.get("consumption_versions", []) if isinstance(changes.get("consumption_versions", []), list) else [],
                    )
            if isinstance(pending_pom_translations, dict) and pending_pom_translations:
                self.save_tech_pack_pom_translations_to_source(pending_pom_translations)
            payload = {
                "ok": True,
                "saved_record": saved_record,
                "synced_count": synced_count,
            }
        except Exception as exc:
            payload = {
                "ok": False,
                "error": str(exc),
                "traceback": traceback.format_exc(limit=10),
            }

        def apply_result() -> None:
            detail_state["save_running"] = False
            if save_button is not None:
                try:
                    save_button.configure(state="normal" if self.is_action_allowed("action.tech_pack.detail_save") else "disabled")
                except Exception:
                    pass
            if not payload.get("ok"):
                if hasattr(self, "append_tech_pack_save_debug_log"):
                    try:
                        self.append_tech_pack_save_debug_log(
                            "detail-save error "
                            + json.dumps(
                                {
                                    "style_code": style_code,
                                    "tab": current_tab,
                                    "message": str(payload.get("error", "")),
                                    "traceback": str(payload.get("traceback", "")),
                                    "changes_summary": self.build_tech_pack_debug_summary(changes) if hasattr(self, "build_tech_pack_debug_summary") else {},
                                },
                                ensure_ascii=False,
                            )
                        )
                    except Exception:
                        pass
                error_message = str(payload.get("error") or "Could not save Tech Pack.")
                messagebox.showerror(self.t("warning_title"), error_message)
                self.set_status(error_message)
                return
            saved_record = payload.get("saved_record")
            if not isinstance(saved_record, dict):
                saved_record = dict(library_by_style.get(style_code, row))
            library_by_style[style_code] = dict(saved_record)
            self.tech_pack_library = list(library_by_style.values())
            if hasattr(self, "append_tech_pack_save_debug_log"):
                try:
                    self.append_tech_pack_save_debug_log(
                        f"detail-save success style={style_code} tab={current_tab} saved_bytes={self.estimate_json_payload_bytes(saved_record) if hasattr(self, 'estimate_json_payload_bytes') else -1}"
                    )
                except Exception:
                    pass
            row.clear()
            row.update(saved_record)
            if current_tab == "overview":
                detail_state["preview_images"] = list(saved_record.get("preview_images", []))
                detail_state["current_preview_index"] = 0
            if hasattr(self, "start_tech_pack_module_refresh_async"):
                self.start_tech_pack_module_refresh_async(silent=True, force=True)
            elif hasattr(self, "refresh_tech_pack_module"):
                self.refresh_tech_pack_module(silent=True)
            if int(payload.get("synced_count", 0) or 0) and hasattr(self, "start_bom_module_refresh_async"):
                self.start_bom_module_refresh_async(silent=True, force=True)
            if current_tab == "consumption" and hasattr(self, "start_production_module_refresh_async"):
                self.start_production_module_refresh_async(silent=True, force=True)
            if callable(detail_state.get("refresh_baby_style_panel")):
                detail_state["refresh_baby_style_panel"]()
            self.toggle_tech_pack_detail_edit_mode(detail_state, False)
            self.set_status(self.t("tech_pack_edit_saved"))

        self.schedule_root_after(0, apply_result)

    threading.Thread(target=worker, daemon=True).start()

def get_tech_pack_detail_tab_override(self, row: dict, tab_key: str) -> str:
    overrides = row.get("detail_tab_overrides", {}) if isinstance(row.get("detail_tab_overrides", {}), dict) else {}
    return str(overrides.get(tab_key, "") or "").strip()

def build_tech_pack_detail_structured_box(
    self,
    parent: tk.Widget,
    *,
    title: str,
    value: str,
    row_index: int,
    column_index: int,
    detail_state: dict,
    field_key: str,
    columnspan: int = 1,
    min_height: int = 40,
    wraplength: int = 240,
) -> None:
    self.build_tech_pack_bom_detail_box(
        parent,
        title=title,
        value=value,
        row_index=row_index,
        column_index=column_index,
        columnspan=columnspan,
        min_height=min_height,
        wraplength=wraplength,
        detail_state=detail_state,
        field_key=field_key,
    )
    detail_state.setdefault("structured_widgets", {})[field_key] = detail_state["bom_widgets"][field_key]

def build_tech_pack_detail_table_cell(
    self,
    parent: tk.Widget,
    *,
    row_index: int,
    column_index: int,
    value: str,
    detail_state: dict,
    field_key: str,
    min_height: int = 36,
    wraplength: int = 180,
    readonly: bool = False,
    font: tuple[str, int] = ("Segoe UI", 8),
    columnspan: int = 1,
) -> None:
    text_widget = tk.Text(
        parent,
        wrap="word",
        height=1 if min_height <= 40 else 2,
        relief="solid",
        bd=1,
        font=font,
        bg="#ffffff",
        fg="#111111",
        padx=4,
        pady=3,
    )
    text_widget.grid(
        row=row_index,
        column=column_index,
        columnspan=columnspan,
        sticky="nsew",
        padx=(0, 1),
        pady=(0, 1),
        ipady=max(0, min_height - 24),
    )
    text_widget.insert("1.0", value)
    if readonly or not detail_state.get("edit_mode"):
        text_widget.configure(state="disabled")
    detail_state.setdefault("text_entries", []).append((text_widget, readonly))
    detail_state.setdefault("structured_widgets", {})[field_key] = text_widget

def build_tech_pack_constructions_tab(self, parent: ttk.Frame, row: dict, detail_state: dict) -> None:
    parent.columnconfigure(0, weight=1)
    items = [item for item in list(row.get("construction_data", [])) if isinstance(item, dict)]
    host = ttk.Frame(parent)
    host.pack(fill="both", expand=True)
    host.columnconfigure(0, weight=1)
    table = ttk.Frame(host)
    table.grid(row=0, column=0, sticky="nsew", padx=8, pady=8)
    columns = [
        ("Sort", 6, "sort"),
        ("Code", 18, "code"),
        ("Type", 18, "type"),
        ("Construction Element", 18, "construction_element"),
        ("Style Specific Placement", 22, "style_specific_placement"),
        ("Construction Details", 28, "details"),
        ("Construction Details 2", 24, "details_2"),
        ("Comments", 18, "comments"),
        ("Ref. Image", 16, "ref_image"),
    ]
    for column_index, (header, weight, _field_name) in enumerate(columns):
        table.columnconfigure(column_index, weight=weight, uniform="construction_table")
        tk.Label(
            table,
            text=header,
            font=("Segoe UI", 9, "bold"),
            bg="#d9d9d9",
            fg="#222222",
            relief="solid",
            bd=1,
            anchor="center",
            padx=4,
            pady=5,
        ).grid(row=0, column=column_index, sticky="ew", padx=(0, 1), pady=(0, 1))

    for index, item in enumerate(items, start=1):
        raw_type = str(item.get("type", "")).strip()
        normalized_type = raw_type
        construction_element = str(item.get("construction_element", "")).strip()
        style_specific_placement = str(item.get("style_specific_placement", "")).strip()
        if raw_type.upper() == "CONSTRUCTION ELEMENT STYLE SPECIFIC PLACEMENT":
            normalized_type = ""
            if not construction_element:
                construction_element = "Construction Element"
            if not style_specific_placement:
                style_specific_placement = "Style Specific Placement"
        values = {
            "sort": str(item.get("sort", "")).strip() or str(index),
            "code": str(item.get("code", "")).strip(),
            "type": normalized_type,
            "construction_element": construction_element,
            "style_specific_placement": style_specific_placement,
            "details": str(item.get("details", "")).strip(),
            "details_2": str(item.get("details_2", "")).strip(),
            "comments": str(item.get("comments", "")).strip(),
            "ref_image": str(item.get("ref_image", "")).strip(),
        }
        for column_index, (_header, _weight, field_name) in enumerate(columns):
            self.build_tech_pack_detail_table_cell(
                table,
                row_index=index,
                column_index=column_index,
                value=values[field_name],
                detail_state=detail_state,
                field_key=f"construction::{index - 1}::{field_name}",
                min_height=44 if field_name in {"details", "details_2", "comments"} else 36,
                wraplength=220 if field_name in {"details", "details_2"} else 160,
                readonly=(field_name == "sort"),
            )

def build_tech_pack_stitching_tab(self, parent: ttk.Frame, row: dict, detail_state: dict) -> None:
    parent.columnconfigure(0, weight=1)
    items = [item for item in list(row.get("stitching_data", [])) if isinstance(item, dict)]
    host = ttk.Frame(parent)
    host.pack(fill="both", expand=True)
    host.columnconfigure(0, weight=1)
    table = ttk.Frame(host)
    table.grid(row=0, column=0, sticky="nsew", padx=8, pady=8)
    columns = [
        ("Sort", 6, "sort"),
        ("Standard Code", 16, "standard_code"),
        ("Standards", 18, "standards"),
        ("Standard Name", 22, "standard_name"),
        ("Description", 28, "description"),
        ("Comment", 20, "comment"),
        ("Ref. Image", 16, "ref_image"),
    ]
    for column_index, (header, weight, _field_name) in enumerate(columns):
        table.columnconfigure(column_index, weight=weight, uniform="stitching_table")
        tk.Label(
            table,
            text=header,
            font=("Segoe UI", 9, "bold"),
            bg="#d9d9d9",
            fg="#222222",
            relief="solid",
            bd=1,
            anchor="center",
            padx=4,
            pady=5,
        ).grid(row=0, column=column_index, sticky="ew", padx=(0, 1), pady=(0, 1))

    for index, item in enumerate(items, start=1):
        values = {
            "sort": str(item.get("sort", "")).strip() or str(index),
            "standard_code": str(item.get("standard_code", "")).strip(),
            "standards": str(item.get("standards", "")).strip(),
            "standard_name": str(item.get("standard_name", "")).strip(),
            "description": str(item.get("description", "")).strip(),
            "comment": str(item.get("comment", "")).strip(),
            "ref_image": str(item.get("ref_image", "")).strip(),
        }
        for column_index, (_header, _weight, field_name) in enumerate(columns):
            self.build_tech_pack_detail_table_cell(
                table,
                row_index=index,
                column_index=column_index,
                value=values[field_name],
                detail_state=detail_state,
                field_key=f"stitching::{index - 1}::{field_name}",
                min_height=44 if field_name in {"description", "comment"} else 36,
                wraplength=220 if field_name in {"standard_name", "description"} else 160,
                readonly=(field_name == "sort"),
            )

@staticmethod
def _is_tech_pack_pom_numeric_token(token: str) -> bool:
    cleaned = str(token).strip().rstrip("\"").rstrip("'").replace(",", "")
    return bool(re.fullmatch(r"-?\d+(?:/\d+)?", cleaned))

@classmethod
def _split_tech_pack_pom_value_tokens(cls, tokens: list[str], expected_count: int) -> list[str]:
    if expected_count <= 0:
        return [" ".join(tokens).strip()] if tokens else []

    from functools import lru_cache

    @lru_cache(maxsize=None)
    def solve(start_index: int, remaining_count: int) -> tuple[str, ...] | None:
        remaining_tokens = len(tokens) - start_index
        if remaining_count == 0:
            return tuple() if start_index == len(tokens) else None
        if remaining_tokens < remaining_count or remaining_tokens > remaining_count * 2:
            return None
        one_token = tokens[start_index].strip()
        if cls._is_tech_pack_pom_numeric_token(one_token):
            tail = solve(start_index + 1, remaining_count - 1)
            if tail is not None:
                return (one_token,) + tail
        if start_index + 1 < len(tokens):
            left = tokens[start_index].strip().rstrip("\"").rstrip("'")
            right = tokens[start_index + 1].strip().rstrip("\"").rstrip("'")
            if re.fullmatch(r"-?\d+", left) and re.fullmatch(r"\d+/\d+", right):
                combined = f"{tokens[start_index].strip()} {tokens[start_index + 1].strip()}"
                tail = solve(start_index + 2, remaining_count - 1)
                if tail is not None:
                    return (combined,) + tail
        return None

    result = solve(0, expected_count)
    if result is not None:
        return [part.strip() for part in result]
    return [" ".join(str(token).strip() for token in tokens if str(token).strip()).strip()]

@staticmethod
def _is_probably_tech_pack_pom_measurement(value: str) -> bool:
    cleaned = str(value).strip().replace('"', "").replace("'", "").replace(",", "")
    if not cleaned:
        return False
    return bool(re.fullmatch(r"-?\d+(?:\s+\d+/\d+|/\d+)?", cleaned))

def _parse_tech_pack_pom_numeric_row(self, line: str, expected_count: int) -> list[str]:
    stripped = str(line).strip()
    if not stripped or re.search(r"[A-Za-z]", stripped):
        return []
    tokens = [token for token in stripped.split() if token]
    values = self._split_tech_pack_pom_value_tokens(tokens, expected_count)
    if expected_count and len(values) != expected_count:
        return []
    if any(not self._is_probably_tech_pack_pom_measurement(value) for value in values):
        return []
    return values

@staticmethod
def _looks_like_tech_pack_pom_row_start(line: str) -> bool:
    stripped = str(line).strip()
    if not stripped:
        return False
    if re.match(
        r"^(Style No:|Style Name:|Points of Measure|Brand:|Request No\b|Designer\b|Model Name\b|Grading Model\b|Grading Method/|Incremental\s*/\s*IN\b|UM\b|POM\b|QA P/S POM Description\b|Code\b)",
        stripped,
        flags=re.IGNORECASE,
    ):
        return False
    if not re.search(r"[A-Za-z]", stripped):
        return False
    return bool(re.match(r"^[A-Z0-9]+(?:-[A-Z0-9]+)?\s+", stripped))

@classmethod
def _parse_tech_pack_pom_row_header(cls, line: str) -> dict | None:
    tokens = [token for token in str(line).strip().split() if token]
    if len(tokens) < 4:
        return None
    code = tokens[0].strip()

    def parse_tail(end_index: int) -> tuple[str, int] | None:
        if end_index <= 0:
            return None
        current = tokens[end_index - 1].strip()
        if cls._is_tech_pack_pom_numeric_token(current):
            return current, end_index - 1
        if end_index >= 2:
            left = tokens[end_index - 2].strip().rstrip("\"").rstrip("'")
            right = tokens[end_index - 1].strip().rstrip("\"").rstrip("'")
            if re.fullmatch(r"-?\d+", left) and re.fullmatch(r"\d+/\d+", right):
                return f"{tokens[end_index - 2].strip()} {tokens[end_index - 1].strip()}", end_index - 2
        return None

    plus_value = parse_tail(len(tokens))
    if plus_value is None:
        return None
    tol_plus, plus_start = plus_value
    minus_value = parse_tail(plus_start)
    if minus_value is None:
        return None
    tol_minus, minus_start = minus_value
    description = " ".join(tokens[1:minus_start]).strip()
    if not description or not re.search(r"[A-Za-z]", description):
        return None
    return {
        "code": code,
        "description": description,
        "tol_minus": tol_minus,
        "tol_plus": tol_plus,
    }

def _tech_pack_pom_rows_need_reparse(self, item: dict, rows: list[dict]) -> bool:
    size_headers = [str(value).strip() for value in list(item.get("size_headers", [])) if str(value).strip()]
    expected_size_count = len(size_headers)
    for row in rows:
        if not isinstance(row, dict):
            return True
        code = str(row.get("code", "")).strip()
        description = str(row.get("description", "")).strip()
        size_values = [str(value).strip() for value in list(row.get("size_values", []))]
        ps_values = [str(value).strip() for value in list(row.get("ps_values", []))]
        if not code or not description:
            return True
        if code == "0" and self._is_probably_tech_pack_pom_measurement(description):
            return True
        if expected_size_count and len(size_values) not in {0, expected_size_count}:
            return True
        if expected_size_count and len(ps_values) not in {0, expected_size_count}:
            return True
        if any(re.search(r"[A-Z]{2,}", value) for value in size_values):
            return True
        if description and self._is_probably_tech_pack_pom_measurement(description) and any(
            re.search(r"[A-Z]{2,}", value) for value in size_values
        ):
            return True
    return False

def extract_tech_pack_pom_rows(self, item: dict) -> list[dict]:
    stored_rows = item.get("table_rows")
    if isinstance(stored_rows, list) and stored_rows:
        normalized_rows: list[dict] = []
        for row in stored_rows:
            if not isinstance(row, dict):
                continue
            normalized_rows.append(
                {
                    "code": str(row.get("code", "")).strip(),
                    "description": str(row.get("description", "")).strip(),
                    "tol_minus": str(row.get("tol_minus", "")).strip(),
                    "tol_plus": str(row.get("tol_plus", "")).strip(),
                    "size_values": [str(value).strip() for value in list(row.get("size_values", []))],
                    "ps_values": [str(value).strip() for value in list(row.get("ps_values", []))],
                }
            )
        if normalized_rows and not self._tech_pack_pom_rows_need_reparse(item, normalized_rows):
            return normalized_rows

    raw_blocks = [str(value).strip() for value in list(item.get("raw_text", [])) if str(value).strip()]
    size_headers = [str(value).strip() for value in list(item.get("size_headers", [])) if str(value).strip()]
    if not size_headers:
        for block in raw_blocks:
            match = re.search(r"Incremental\s*/\s*IN\s+(.+)", block)
            if match:
                size_headers = [part.strip() for part in match.group(1).split() if part.strip()]
                if size_headers:
                    break
    rows: list[dict] = []
    size_count = len(size_headers)
    for block in raw_blocks:
        lines = [line.strip() for line in str(block).splitlines() if line.strip()]
        try:
            start_index = next(index for index, line in enumerate(lines) if line.strip().upper() == "CODE") + 1
        except StopIteration:
            continue
        index = start_index
        pending_ps_values: list[str] = []
        while index < len(lines):
            line = lines[index].strip()
            numeric_values = self._parse_tech_pack_pom_numeric_row(line, size_count)
            next_line = lines[index + 1].strip() if index + 1 < len(lines) else ""
            if numeric_values and self._looks_like_tech_pack_pom_row_start(next_line):
                pending_ps_values = numeric_values
                index += 1
                continue
            if not self._looks_like_tech_pack_pom_row_start(line):
                index += 1
                continue
            header = self._parse_tech_pack_pom_row_header(line)
            if header is None:
                index += 1
                continue
            size_line = lines[index + 1].strip() if index + 1 < len(lines) else ""
            size_values = self._split_tech_pack_pom_value_tokens(size_line.split(), size_count) if size_line and size_count else ([size_line] if size_line else [])
            ps_values = list(pending_ps_values)
            pending_ps_values = []
            rows.append(
                {
                    "code": header["code"],
                    "description": header["description"],
                    "tol_minus": header["tol_minus"],
                    "tol_plus": header["tol_plus"],
                    "size_values": size_values,
                    "ps_values": ps_values,
                }
            )
            index += 2
    if size_headers:
        item["size_headers"] = size_headers
    if rows:
        item["table_rows"] = rows
    return rows

def build_tech_pack_instructions_tab(self, parent: ttk.Frame, row: dict, detail_state: dict) -> None:
    parent.columnconfigure(0, weight=1)
    host = ttk.Frame(parent)
    host.pack(fill="both", expand=True)
    host.columnconfigure(0, weight=1)
    intents = [item for item in list(row.get("design_intent_data", [])) if isinstance(item, dict)]
    attachments = [item for item in list(row.get("attachments_data", [])) if isinstance(item, dict)]
    current_row = 0
    for index, item in enumerate(intents):
        block = ttk.Frame(host)
        block.grid(row=current_row, column=0, sticky="ew", padx=8, pady=(0 if current_row == 0 else 10, 8))
        block.columnconfigure(0, weight=1)
        self.build_tech_pack_detail_structured_box(block, title=f"Design Intent {index + 1}", value=str(item.get("details", "")).strip(), row_index=0, column_index=0, min_height=80, wraplength=1080, detail_state=detail_state, field_key=f"intent::{index}::details")
        current_row += 1
    if attachments:
        attach_block = ttk.Frame(host)
        attach_block.grid(row=current_row, column=0, sticky="ew", padx=8, pady=(10, 8))
        for col in range(2):
            attach_block.columnconfigure(col, weight=1)
        for index, item in enumerate(attachments):
            self.build_tech_pack_detail_structured_box(attach_block, title=f"Attachment Page {index + 1}", value=str(item.get("page_number", "")).strip(), row_index=index, column_index=0, detail_state=detail_state, field_key=f"attachment::{index}::page_number")
            self.build_tech_pack_detail_structured_box(attach_block, title="Title", value=str(item.get("title", "")).strip(), row_index=index, column_index=1, detail_state=detail_state, field_key=f"attachment::{index}::title")
        current_row += 1
    notes_value = str(row.get("instructions_notes", "")).strip()
    notes_block = ttk.Frame(host)
    notes_block.grid(row=current_row, column=0, sticky="ew", padx=8, pady=(10, 8))
    notes_block.columnconfigure(0, weight=1)
    self.build_tech_pack_detail_structured_box(notes_block, title="Instructions Notes", value=notes_value, row_index=0, column_index=0, min_height=80, wraplength=1080, detail_state=detail_state, field_key="record::0::instructions_notes")

def build_tech_pack_pom_tab(self, parent: ttk.Frame, row: dict, detail_state: dict) -> None:
    parent.columnconfigure(0, weight=1)
    parent.rowconfigure(0, weight=1)
    scroll_host = ttk.Frame(parent)
    scroll_host.pack(fill="both", expand=True)
    scroll_host.columnconfigure(0, weight=1)
    scroll_host.rowconfigure(0, weight=1)
    canvas = tk.Canvas(scroll_host, highlightthickness=0, bg="#ffffff")
    scrollbar = ttk.Scrollbar(scroll_host, orient="vertical", command=canvas.yview)
    canvas.grid(row=0, column=0, sticky="nsew")
    scrollbar.grid(row=0, column=1, sticky="ns")
    canvas.configure(yscrollcommand=scrollbar.set)
    host = ttk.Frame(canvas)
    host.columnconfigure(0, weight=1)
    canvas_window = canvas.create_window((0, 0), window=host, anchor="nw")

    def sync_scroll_region(_event=None) -> None:
        canvas.configure(scrollregion=canvas.bbox("all"))

    def sync_window_width(event) -> None:
        canvas.itemconfigure(canvas_window, width=event.width)

    def on_mousewheel(event) -> None:
        delta = event.delta
        if delta == 0:
            return
        canvas.yview_scroll(int(-delta / 120), "units")

    host.bind("<Configure>", sync_scroll_region)
    canvas.bind("<Configure>", sync_window_width)
    canvas.bind("<Enter>", lambda _event: canvas.bind_all("<MouseWheel>", on_mousewheel))
    canvas.bind("<Leave>", lambda _event: canvas.unbind_all("<MouseWheel>"))

    translations = self.load_tech_pack_pom_translations_from_source()
    items = [item for item in list(row.get("pom_data", [])) if isinstance(item, dict)]
    for index, item in enumerate(items):
        block = ttk.Frame(host)
        block.grid(row=index, column=0, sticky="ew", padx=8, pady=(0 if index == 0 else 10, 8))
        for col in range(3):
            block.columnconfigure(col, weight=1)
        self.build_tech_pack_detail_structured_box(block, title="Model Name", value=str(item.get("model_name", "")).strip(), row_index=0, column_index=0, detail_state=detail_state, field_key=f"pom::{index}::model_name")
        self.build_tech_pack_detail_structured_box(block, title="Grading Model", value=str(item.get("grading_model", "")).strip(), row_index=0, column_index=1, detail_state=detail_state, field_key=f"pom::{index}::grading_model")
        self.build_tech_pack_detail_structured_box(block, title="Pages", value=", ".join(str(page) for page in list(item.get("pages", []))), row_index=0, column_index=2, detail_state=detail_state, field_key=f"pom::{index}::pages")

        size_headers = [str(value).strip() for value in list(item.get("size_headers", [])) if str(value).strip()]
        rows_data = self.extract_tech_pack_pom_rows(item)
        if not size_headers:
            size_headers = [str(value).strip() for value in list(item.get("size_headers", [])) if str(value).strip()]

        if rows_data and size_headers:
            table = ttk.Frame(block)
            table.grid(row=1, column=0, columnspan=3, sticky="nsew", pady=(8, 0))
            columns = [
                ("POM Code", 10, "code"),
                ("Description", 28, "description"),
                ("Translate", 28, "translate"),
                ("Tol (-)", 8, "tol_minus"),
                ("Tol (+)", 8, "tol_plus"),
            ] + [(header, 9, f"size_{size_index}") for size_index, header in enumerate(size_headers)]
            for column_index, (header, weight, _field_name) in enumerate(columns):
                table.columnconfigure(column_index, weight=weight, uniform=f"pom_table_{index}")
                tk.Label(
                    table,
                    text=header,
                    font=("Segoe UI", 9, "bold"),
                    bg="#d9d9d9",
                    fg="#222222",
                    relief="solid",
                    bd=1,
                    anchor="center",
                    padx=4,
                    pady=5,
                ).grid(row=0, column=column_index, sticky="ew", padx=(0, 1), pady=(0, 1))

            current_row = 1
            for row_index, row_item in enumerate(rows_data):
                ps_values = [str(value).strip() for value in list(row_item.get("ps_values", []))]
                if ps_values:
                    while len(ps_values) < len(size_headers):
                        ps_values.append("")
                    self.build_tech_pack_detail_table_cell(
                        table,
                        row_index=current_row,
                        column_index=0,
                        value="",
                        detail_state=detail_state,
                        field_key=f"pomsub::{index}::{row_index}::__merged_blank",
                        readonly=True,
                        font=("Segoe UI", 9),
                        columnspan=5,
                        min_height=28,
                    )
                    for size_index, ps_value in enumerate(ps_values[: len(size_headers)]):
                        self.build_tech_pack_detail_table_cell(
                            table,
                            row_index=current_row,
                            column_index=5 + size_index,
                            value=ps_value,
                            detail_state=detail_state,
                            field_key=f"pomsub::{index}::{row_index}::ps_{size_index}",
                            min_height=30,
                            wraplength=120,
                            font=("Segoe UI", 9),
                        )
                    current_row += 1
                values = {
                    "code": str(row_item.get("code", "")).strip(),
                    "description": str(row_item.get("description", "")).strip(),
                    "translate": str(
                        translations.get(
                            self.normalize_tech_pack_pom_translation_key(row_item.get("description", "")),
                            {},
                        ).get("vi", "")
                    ).strip(),
                    "tol_minus": str(row_item.get("tol_minus", "")).strip(),
                    "tol_plus": str(row_item.get("tol_plus", "")).strip(),
                }
                size_values = [str(value).strip() for value in list(row_item.get("size_values", []))]
                while len(size_values) < len(size_headers):
                    size_values.append("")
                for size_index, size_value in enumerate(size_values[: len(size_headers)]):
                    values[f"size_{size_index}"] = size_value
                for column_index, (_header, _weight, field_name) in enumerate(columns):
                    self.build_tech_pack_detail_table_cell(
                        table,
                        row_index=current_row,
                        column_index=column_index,
                        value=values.get(field_name, ""),
                        detail_state=detail_state,
                        field_key=f"pomrow::{index}::{row_index}::{field_name}",
                        min_height=40 if field_name in {"description", "translate"} else 34,
                        wraplength=220 if field_name in {"description", "translate"} else 120,
                        font=("Segoe UI", 9),
                    )
                current_row += 1
        else:
            self.build_tech_pack_detail_structured_box(
                block,
                title="Raw Text",
                value="\n\n".join(str(value).strip() for value in list(item.get("raw_text", [])) if str(value).strip()),
                row_index=1,
                column_index=0,
                columnspan=3,
                min_height=140,
                wraplength=1080,
                detail_state=detail_state,
                field_key=f"pom::{index}::raw_text",
            )

def get_tech_pack_linked_samples(self, row: dict) -> list[dict]:
    style_code = self.display_value(row.get("style_code"))
    style_identity = str(row.get("style_identity", "")).strip()
    if not style_identity and hasattr(self, "build_style_identity"):
        overview_data = row.get("overview_data", {}) if isinstance(row.get("overview_data", {}), dict) else {}
        style_identity = self.build_style_identity(
            row.get("style_code"),
            overview_data.get("season") or row.get("season_base") or row.get("season"),
            overview_data.get("season_year") or row.get("season_year"),
        )
    detail_title = f"{self.t('tech_pack_detail_title')} - {style_identity or style_code}"
    normalized_style = normalize_style_identifier(style_code)
    if not normalized_style:
        return []
    sample_rows = self.load_sample_records_from_source()
    return [
        sample_row
        for sample_row in sample_rows
        if normalize_style_identifier(sample_row.get("style_code") or sample_row.get("style")) == normalized_style
    ]


def get_tech_pack_consumption_versions(self, row: dict) -> list[dict]:
    style_code = normalize_style_identifier(row.get("style_code"))
    existing_versions = row.get("consumption_versions", []) if isinstance(row.get("consumption_versions", []), list) else []
    if not existing_versions and style_code:
        library_record = next(
            (
                dict(item)
                for item in list(getattr(self, "tech_pack_library", []) or [])
                if normalize_style_identifier(item.get("style_code")) == style_code
            ),
            {},
        )
        existing_versions = (
            library_record.get("consumption_versions", [])
            if isinstance(library_record.get("consumption_versions", []), list)
            else []
        )
    existing_by_type: dict[str, dict] = {}
    for item in existing_versions:
        if not isinstance(item, dict):
            continue
        version_type = str(item.get("version_type", "")).strip().lower()
        if version_type:
            existing_by_type[version_type] = dict(item)

    versions: list[dict] = []
    for version_type, version_name in TECH_PACK_CONSUMPTION_VERSION_TEMPLATES:
        source = existing_by_type.get(version_type, {})
        normalized_lines: list[dict] = []
        for index, line in enumerate(list(source.get("lines", [])), start=1):
            if not isinstance(line, dict):
                continue
            normalized_lines.append(_enrich_consumption_line_from_bom(self, line, style_code, version_type, index))
        versions.append(
            {
                "version_id": str(source.get("version_id", "")).strip() or version_type,
                "version_type": version_type,
                "version_name": str(source.get("version_name", "")).strip() or version_name,
                "lines": normalized_lines,
            }
        )
    return versions


def get_tech_pack_consumption_bom_candidates(self, style_code: str, version_type: object = "") -> list[dict]:
    normalized_style = normalize_style_identifier(style_code)
    normalized_version_type = str(version_type or "").strip().lower()
    candidates = [dict(item) for item in list(getattr(self, "bom_items", [])) if isinstance(item, dict)]
    if not candidates and hasattr(self, "refresh_bom_module"):
        try:
            self.refresh_bom_module()
        except Exception:
            pass
        candidates = [dict(item) for item in list(getattr(self, "bom_items", [])) if isinstance(item, dict)]
    if normalized_version_type not in {"costing", "booking", "production", "settlement"} and normalized_style:
        style_candidates = [
            item for item in candidates
            if _style_code_matches_bom_style_list(item.get("style_code"), normalized_style)
        ]
        if style_candidates:
            candidates = style_candidates
    if normalized_version_type == "costing":
        allowed_statuses = {"draft", "checking", "approved"}
    else:
        allowed_statuses = {"approved"}
    return [
        item for item in candidates
        if str(item.get("status", "")).strip().lower() in allowed_statuses
        and (
            str(item.get("item_name", "")).strip()
            or str(item.get("composition", "")).strip()
            or str(item.get("item_code", "")).strip()
        )
    ]


def open_select_tech_pack_consumption_bom_item_dialog(
    self,
    style_code: str,
    anchor_widget: tk.Widget | None = None,
    version_type: object = "",
) -> dict | None:
    normalized_style = normalize_style_identifier(style_code)
    normalized_version_type = str(version_type or "").strip().lower()
    parent_window: tk.Misc = self.root
    if anchor_widget is not None:
        try:
            candidate_parent = anchor_widget.winfo_toplevel()
            if candidate_parent is not None and int(candidate_parent.winfo_exists()) == 1:
                parent_window = candidate_parent
        except Exception:
            parent_window = self.root
    candidates = self.get_tech_pack_consumption_bom_candidates(style_code, normalized_version_type)
    if not candidates:
        if normalized_version_type == "costing":
            messagebox.showinfo("Consumption", "No draft/checking/approved BOM item is available yet.", parent=parent_window)
        else:
            messagebox.showinfo("Consumption", "No approved BOM item is available yet.", parent=parent_window)
        return None

    dialog = tk.Toplevel(parent_window)
    dialog.title(f"Select BOM Item - {(normalized_version_type or 'consumption').title()}")
    dialog.transient(parent_window)
    dialog.resizable(False, False)

    def close_dialog() -> None:
        try:
            if dialog.grab_current() is dialog:
                dialog.grab_release()
        except Exception:
            pass
        try:
            dialog.destroy()
        except tk.TclError:
            pass

    dialog.protocol("WM_DELETE_WINDOW", close_dialog)
    try:
        dialog.grab_set()
    except tk.TclError:
        pass

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

    ttk.Label(
        frame,
        text=(
            "Search one draft/checking/approved BOM item for Costing."
            if normalized_version_type == "costing"
            else "Search one approved BOM item from the shared BOM list."
            if normalized_version_type in {"booking", "production", "settlement"}
            else (
                f"Search one BOM item for style {normalized_style}."
                if normalized_style
                else "Search one BOM item."
            )
        ),
    ).grid(row=0, column=0, sticky="w")

    selected_var = tk.StringVar(value="")
    search_var = tk.StringVar(value="")
    search_entry = ttk.Entry(frame, textvariable=search_var, width=52)
    search_entry.grid(row=1, column=0, sticky="ew", pady=(8, 0))

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

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

    filtered_candidates: list[dict] = []
    result: dict[str, dict | None] = {"item": None}

    def format_candidate(item: dict) -> str:
        style_value = str(item.get("style_code", "")).strip() or "-"
        item_code = str(item.get("item_code", "")).strip() or "(No item code)"
        item_name = str(item.get("item_name", "")).strip() or "(No item name)"
        item_type = str(item.get("item_type", "")).strip() or str(item.get("section", "")).strip() or "-"
        color_value = str(item.get("color", "")).strip() or "-"
        trim_size_value = str(item.get("trim_size_um", "")).strip() or "-"
        length_value = str(item.get("length_value", "")).strip() or "-"
        width_value = str(item.get("width_value", "")).strip() or "-"
        gsm_value = str(item.get("gsm_value", "")).strip() or "-"
        return (
            f"{style_value} | {item_code} | {item_name} | {item_type}"
            f" | Status: {str(item.get('status', '')).strip() or '-'}"
            f" | Color: {color_value} | Trim Size: {trim_size_value}"
            f" | Length: {length_value} | Width: {width_value} | GSM: {gsm_value}"
        )

    def get_filtered_candidates() -> list[dict]:
        query = search_var.get().strip().lower()
        if not query:
            return list(candidates)
        return [
            item for item in candidates
            if query in " ".join(
                part for part in (
                    normalize_style_identifier(item.get("style_code")),
                    str(item.get("item_code", "")).strip(),
                    str(item.get("item_name", "")).strip(),
                    str(item.get("status", "")).strip(),
                    str(item.get("color", "")).strip(),
                    str(item.get("trim_size_um", "")).strip(),
                    str(item.get("length_value", "")).strip(),
                    str(item.get("width_value", "")).strip(),
                    str(item.get("gsm_value", "")).strip(),
                )
                if part
            ).lower()
        ]

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

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

    def save_selection() -> None:
        selected_item_id = selected_var.get().strip()
        if not selected_item_id:
            messagebox.showwarning("Consumption", "Please choose one BOM item from the search results.", parent=dialog)
            return
        chosen = next(
            (item for item in filtered_candidates if str(item.get("item_id", "")).strip() == selected_item_id),
            None,
        )
        if chosen is None:
            chosen = next(
                (item for item in candidates if str(item.get("item_id", "")).strip() == selected_item_id),
                None,
            )
        if isinstance(chosen, dict) and not str(chosen.get("item_code", "")).strip():
            messagebox.showwarning("Consumption", "BOM item must have Item Code before it can be used in consumption.", parent=dialog)
            return
        result["item"] = dict(chosen) if isinstance(chosen, dict) else None
        close_dialog()

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

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

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


def build_tech_pack_consumption_tab(self, parent: ttk.Frame, row: dict, detail_state: dict) -> None:
    parent.columnconfigure(0, weight=1)
    parent.rowconfigure(0, weight=1)

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

    canvas = tk.Canvas(scroll_host, highlightthickness=0, bg="#ffffff")
    canvas.grid(row=0, column=0, sticky="nsew")
    scrollbar = ttk.Scrollbar(scroll_host, orient="vertical", command=canvas.yview)
    scrollbar.grid(row=0, column=1, sticky="ns")
    canvas.configure(yscrollcommand=scrollbar.set)

    body = ttk.Frame(canvas)
    window_id = canvas.create_window((0, 0), window=body, anchor="nw")

    def sync_width(event: tk.Event) -> None:
        canvas.itemconfigure(window_id, width=event.width)

    def update_scrollregion(_event: tk.Event) -> None:
        canvas.configure(scrollregion=canvas.bbox("all"))

    def on_mousewheel(event: tk.Event) -> str:
        canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
        return "break"

    def bind_mousewheel(_event: tk.Event) -> None:
        canvas.bind_all("<MouseWheel>", on_mousewheel)

    def unbind_mousewheel(_event: tk.Event) -> None:
        canvas.unbind_all("<MouseWheel>")

    canvas.bind("<Configure>", sync_width)
    body.bind("<Configure>", update_scrollregion)
    for widget in (canvas, body, parent):
        widget.bind("<Enter>", bind_mousewheel)
        widget.bind("<Leave>", unbind_mousewheel)

    body.columnconfigure(0, weight=1)
    detail_state["consumption_versions_state"] = []
    detail_state["consumption_buttons"] = []
    detail_state["consumption_combo_widgets"] = []
    detail_state["consumption_field_entries"] = []

    intro = ttk.Label(
        body,
        text="Manage consumption by version. Use + to add one BOM item with Item Code, then type the consumption value manually.",
        foreground="#5f6b7a",
        wraplength=1080,
        justify="left",
    )
    intro.grid(row=0, column=0, sticky="w", padx=8, pady=(8, 12))

    style_code = normalize_style_identifier(row.get("style_code"))

    def build_version_line_row(host: ttk.Frame, version_state: dict, line_state: dict, row_index: int) -> None:
        readonly_fields = [
            ("costing_id", 20),
            ("bom_item_code", 18),
            ("fabric_type", 18),
            ("section", 14),
            ("color", 18),
            ("trim_size_um", 12),
            ("length_value", 14),
            ("width_value", 14),
            ("gsm_value", 12),
            ("waste_percent", 10),
            ("fabric_weight", 12),
            ("bom_item_name", 30),
        ]
        for column_index, (field_name, width) in enumerate(readonly_fields):
            variable = tk.StringVar(value=str(line_state.get(field_name, "")).strip())
            entry = tk.Entry(
                host,
                textvariable=variable,
                relief="solid",
                bd=1,
                font=("Segoe UI", 9),
                readonlybackground="#ededed",
                disabledbackground="#ededed",
                fg="#333333",
                width=width,
            )
            entry.grid(row=row_index, column=column_index, sticky="ew", padx=(0, 6), pady=(0, 6), ipady=2)
            entry.configure(state="readonly")
            detail_state.setdefault("field_entries", []).append((entry, True))
            detail_state.setdefault("consumption_field_entries", []).append((entry, True))

        consumption_var = tk.StringVar(value=str(line_state.get("consumption", "")).strip())
        line_state["consumption_var"] = consumption_var
        consumption_entry = tk.Entry(
            host,
            textvariable=consumption_var,
            relief="solid",
            bd=1,
            font=("Segoe UI", 9),
            bg="#ffffff",
            fg="#333333",
            width=14,
        )
        consumption_entry.grid(row=row_index, column=10, sticky="ew", padx=(0, 6), pady=(0, 6), ipady=2)
        consumption_entry.configure(state="normal" if detail_state.get("edit_mode") else "readonly")
        detail_state.setdefault("field_entries", []).append((consumption_entry, False))
        detail_state.setdefault("consumption_field_entries", []).append((consumption_entry, False))

        uom_var = tk.StringVar(value=str(line_state.get("uom", "")).strip())
        line_state["uom_var"] = uom_var
        uom_combo = ttk.Combobox(
            host,
            textvariable=uom_var,
            values=_consumption_uom_options_for_item_type(line_state.get("fabric_type", ""), line_state.get("uom", "")),
            state="disabled",
            width=10,
        )
        uom_combo.grid(row=row_index, column=11, sticky="ew", padx=(0, 6), pady=(0, 6))
        detail_state.setdefault("consumption_combo_widgets", []).append(uom_combo)

        remove_button = ttk.Button(
            host,
            text="-",
            width=3,
            command=lambda line_ref=line_state, version_ref=version_state: remove_consumption_line(version_ref, line_ref),
        )
        remove_button.grid(row=row_index, column=12, sticky="w", pady=(0, 6))
        remove_button.configure(state="normal" if detail_state.get("edit_mode") else "disabled")
        detail_state.setdefault("consumption_buttons", []).append(remove_button)

    def refresh_versions() -> None:
        previous_consumption_entries = list(detail_state.get("consumption_field_entries", []))
        if previous_consumption_entries:
            detail_state["field_entries"] = [
                item for item in list(detail_state.get("field_entries", []))
                if item not in previous_consumption_entries
            ]
        detail_state["consumption_field_entries"] = []
        for child in body.winfo_children():
            if child is not intro:
                child.destroy()
        intro.grid(row=0, column=0, sticky="w", padx=8, pady=(8, 12))
        detail_state["consumption_buttons"] = []
        detail_state["consumption_combo_widgets"] = []
        for version_index, version_state in enumerate(detail_state.get("consumption_versions_state", []), start=1):
            card = ttk.LabelFrame(body, text=str(version_state.get("version_name", "")).strip() or "Version")
            card.grid(row=version_index, column=0, sticky="ew", padx=8, pady=(0, 12))
            card.columnconfigure(0, weight=1)

            header = ttk.Frame(card)
            header.grid(row=0, column=0, sticky="ew", padx=10, pady=(10, 8))
            header.columnconfigure(0, weight=1)
            ttk.Label(
                header,
                text=str(version_state.get("version_type", "")).strip().title(),
                foreground="#5f6b7a",
            ).grid(row=0, column=0, sticky="w")
            version_type = str(version_state.get("version_type", "")).strip().lower()
            header_button_column = 1
            if version_type == "production":
                copy_booking_button = ttk.Button(
                    header,
                    text="Copy from Booking",
                    command=lambda version_ref=version_state: copy_consumption_lines_from_version("booking", version_ref),
                )
                copy_booking_button.grid(row=0, column=header_button_column, sticky="e", padx=(0, 6))
                copy_booking_button.configure(state="normal" if detail_state.get("edit_mode") else "disabled")
                detail_state.setdefault("consumption_buttons", []).append(copy_booking_button)
                header_button_column += 1
            elif version_type == "settlement":
                copy_production_button = ttk.Button(
                    header,
                    text="Copy from Production",
                    command=lambda version_ref=version_state: copy_consumption_lines_from_version("production", version_ref),
                )
                copy_production_button.grid(row=0, column=header_button_column, sticky="e", padx=(0, 6))
                copy_production_button.configure(state="normal" if detail_state.get("edit_mode") else "disabled")
                detail_state.setdefault("consumption_buttons", []).append(copy_production_button)
                header_button_column += 1
            add_button = ttk.Button(header, text="+", width=3)
            add_button.grid(row=0, column=header_button_column, sticky="e")
            add_button.configure(
                state="normal" if detail_state.get("edit_mode") else "disabled",
                command=lambda version_ref=version_state, button_ref=add_button: add_consumption_line(version_ref, button_ref),
            )
            detail_state.setdefault("consumption_buttons", []).append(add_button)

            table = ttk.Frame(card)
            table.grid(row=1, column=0, sticky="ew", padx=10, pady=(0, 10))
            for column_index in range(12):
                table.columnconfigure(column_index, weight=1)
            headers = ["Costing ID", "Item Code", "Type", "Section", "Color", "Trim Size", "Length", "Width", "GSM", "Waste (%)", "Consumption", "UOM"]
            for column_index, title in enumerate(headers):
                ttk.Label(table, text=title).grid(row=0, column=column_index, sticky="w", padx=(0, 6), pady=(0, 6))

            lines = list(version_state.get("lines", []))
            if not lines:
                ttk.Label(table, text="No consumption line yet.", foreground="#7a8698").grid(
                    row=1,
                    column=0,
                    columnspan=12,
                    sticky="w",
                )
            else:
                for line_index, line_state in enumerate(lines, start=1):
                    build_version_line_row(table, version_state, line_state, line_index)

    def add_consumption_line(version_state: dict, anchor_widget: tk.Widget | None = None) -> None:
        if not detail_state.get("edit_mode"):
            self.set_status("Enable edit mode before adding consumption lines.")
            return
        selected_item = self.open_select_tech_pack_consumption_bom_item_dialog(
            style_code,
            anchor_widget,
            version_state.get("version_type", ""),
        )
        if not isinstance(selected_item, dict):
            return
        line_id = uuid.uuid4().hex
        version_state.setdefault("lines", []).append(
            _build_consumption_line_from_bom(
                selected_item,
                style_code=style_code,
                version_type=version_state.get("version_type", ""),
                line_index=len(list(version_state.get("lines", []))) + 1,
                line_id=line_id,
            )
        )
        refresh_versions()

    def copy_consumption_lines_from_version(source_version_type: str, target_version_state: dict) -> None:
        if not detail_state.get("edit_mode"):
            self.set_status("Enable edit mode before copying consumption lines.")
            return
        normalized_source_type = str(source_version_type or "").strip().lower()
        source_version_state = next(
            (
                item
                for item in list(detail_state.get("consumption_versions_state", []))
                if str(item.get("version_type", "")).strip().lower() == normalized_source_type
            ),
            None,
        )
        if not isinstance(source_version_state, dict):
            messagebox.showinfo("Consumption", f"Source version {normalized_source_type.title()} was not found.")
            return
        source_lines = [dict(line) for line in list(source_version_state.get("lines", [])) if isinstance(line, dict)]
        if not source_lines:
            messagebox.showinfo("Consumption", f"No line exists in {normalized_source_type.title()} yet.")
            return
        target_lines: list[dict] = []
        target_version_type = str(target_version_state.get("version_type", "")).strip()
        for line_index, source_line in enumerate(source_lines, start=1):
            copied_line = dict(source_line)
            new_line_id = uuid.uuid4().hex
            copied_line["line_id"] = new_line_id
            copied_line["costing_id"] = _build_consumption_costing_id(
                style_code,
                target_version_type,
                line_index,
                new_line_id,
            )
            target_lines.append(
                _enrich_consumption_line_from_bom(
                    self,
                    copied_line,
                    style_code,
                    target_version_type,
                    line_index,
                )
            )
        target_version_state["lines"] = target_lines
        refresh_versions()

    def remove_consumption_line(version_state: dict, line_state: dict) -> None:
        if not detail_state.get("edit_mode"):
            self.set_status("Enable edit mode before removing consumption lines.")
            return
        version_state["lines"] = [
            item for item in list(version_state.get("lines", []))
            if item is not line_state
        ]
        refresh_versions()

    for version in self.get_tech_pack_consumption_versions(row):
        detail_state["consumption_versions_state"].append(
            {
                "version_id": str(version.get("version_id", "")).strip(),
                "version_type": str(version.get("version_type", "")).strip(),
                "version_name": str(version.get("version_name", "")).strip(),
                "lines": [dict(line) for line in list(version.get("lines", [])) if isinstance(line, dict)],
            }
        )

    refresh_versions()


def build_tech_pack_samples_tab(self, parent: ttk.Frame, row: dict, detail_state: dict) -> None:
    parent.columnconfigure(0, weight=1)
    parent.rowconfigure(1, weight=1)
    host = ttk.Frame(parent)
    host.pack(fill="both", expand=True)
    host.columnconfigure(0, weight=1)
    host.rowconfigure(1, weight=1)

    linked_rows = self.get_tech_pack_linked_samples(row)

    note_label = ttk.Label(
        host,
        text="Linked sample records from the main Samples tab for this style.",
        foreground="#5f6b7a",
    )
    note_label.grid(row=0, column=0, sticky="w", pady=(0, 10))

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

    tree = ttk.Treeview(table_frame, columns=TECH_PACK_SAMPLES_LINK_COLUMNS, show="headings", selectmode="browse", height=12)
    tree.configure(style="Costing.Treeview")
    column_meta = {
        "created_at": (self.t("create_time_column"), 145, "center"),
        "request_number": (self.t("samples_request_column"), 120, "center"),
        "mr_pic": (self.t("samples_mr_pic_column"), 100, "center"),
        "pattern_pic": (self.t("samples_pattern_pic_column"), 100, "center"),
        "sewer_pic": (self.t("samples_sewer_pic_column"), 100, "center"),
        "ie_pic": (self.t("samples_ie_pic_column"), 100, "center"),
        "sample_type": (self.t("samples_type_column"), 110, "center"),
        "color": (self.t("samples_color_column"), 150, "w"),
        "size": (self.t("samples_size_column"), 100, "center"),
        "quantity": (self.t("samples_quantity_column"), 110, "center"),
        "finishing_date": (self.t("samples_finishing_date_column"), 120, "center"),
        "buyer_comment_date": (self.t("samples_buyer_comment_date_column"), 140, "center"),
    }
    for column_key in TECH_PACK_SAMPLES_LINK_COLUMNS:
        title, width, anchor = column_meta[column_key]
        tree.heading(column_key, text=title)
        tree.column(column_key, width=width, anchor=anchor, stretch=False)
    configure_striped_treeview(tree)

    scroll_y = ttk.Scrollbar(table_frame, orient="vertical", command=tree.yview)
    scroll_x = ttk.Scrollbar(table_frame, orient="horizontal", command=tree.xview)
    tree.configure(yscrollcommand=scroll_y.set, xscrollcommand=scroll_x.set)
    tree.grid(row=0, column=0, sticky="nsew")
    scroll_y.grid(row=0, column=1, sticky="ns")
    scroll_x.grid(row=1, column=0, sticky="ew")

    for index, sample_row in enumerate(linked_rows, start=1):
        tree.insert(
            "",
            "end",
            values=(
                self.display_value(sample_row.get("created_at")),
                self.display_value(sample_row.get("request_number")),
                self.display_value(sample_row.get("mr_pic")),
                self.display_value(sample_row.get("pattern_pic")),
                self.display_value(sample_row.get("sewer_pic")),
                self.display_value(sample_row.get("ie_pic")),
                self.display_value(sample_row.get("sample_type")),
                self.display_value(sample_row.get("color")),
                self.display_value(sample_row.get("size")),
                self.display_value(sample_row.get("quantity")),
                self.display_value(sample_row.get("finishing_date")),
                self.display_value(sample_row.get("buyer_comment_date")),
            ),
            tags=("odd_row" if index % 2 == 1 else "even_row",),
        )

    if not linked_rows:
        empty_label = ttk.Label(
            table_frame,
            text="No linked sample records were found for this style yet.",
            foreground="#7a8698",
        )
        empty_label.place(relx=0.5, rely=0.5, anchor="center")

def build_tech_pack_attachments_tab(self, parent: ttk.Frame, row: dict, detail_state: dict) -> None:
    detail_state["attachments_tab_parent"] = parent
    parent.columnconfigure(0, weight=1)
    host = ttk.Frame(parent)
    host.pack(fill="both", expand=True)
    host.columnconfigure(0, weight=1)
    host.rowconfigure(1, weight=1)

    source_files = [str(item).strip() for item in list(row.get("source_files", [])) if str(item).strip()]
    attachments = [item for item in list(row.get("attachments_data", [])) if isinstance(item, dict)]

    source_section = ttk.LabelFrame(host, text="Imported Tech Pack Files")
    source_section.grid(row=0, column=0, sticky="ew", padx=8, pady=(8, 10))
    source_section.columnconfigure(0, weight=1)

    ttk.Label(
        source_section,
        text="Danh sach Tech Pack da import cua style nay. Bam Download de mo file nguon tren local hoac server.",
        foreground="#5f6b7a",
        wraplength=1020,
        justify="left",
    ).grid(row=0, column=0, sticky="w", padx=10, pady=(8, 10))

    if source_files:
        for index, source_file in enumerate(source_files, start=1):
            row_frame = ttk.Frame(source_section)
            row_frame.grid(row=index, column=0, sticky="ew", padx=10, pady=(0, 8))
            row_frame.columnconfigure(0, weight=1)
            ttk.Label(
                row_frame,
                text=Path(source_file.replace("\\", "/")).name or source_file,
                anchor="w",
            ).grid(row=0, column=0, sticky="ew", padx=(0, 10))
            ttk.Button(
                row_frame,
                text="Download",
                command=lambda value=source_file: self.open_tech_pack_attachment_file(value),
            ).grid(row=0, column=1, sticky="e")
            delete_button = ttk.Button(
                row_frame,
                text="Delete",
                command=lambda value=source_file, item_index=index - 1: self.delete_tech_pack_source_file(row, value, detail_state, item_index),
            )
            delete_button.grid(row=0, column=2, sticky="e", padx=(8, 0))
            delete_button.configure(state="normal" if self.is_action_allowed("action.tech_pack.detail_save") else "disabled")
    else:
        ttk.Label(
            source_section,
            text="Chua co file Tech Pack nguon nao duoc gan cho style nay.",
            foreground="#7a8698",
        ).grid(row=1, column=0, sticky="w", padx=10, pady=(0, 10))

    attachment_section = ttk.LabelFrame(host, text="Attachment Pages")
    attachment_section.grid(row=1, column=0, sticky="nsew", padx=8, pady=(0, 8))
    attachment_section.columnconfigure(0, weight=1)
    attachment_section.columnconfigure(1, weight=0)
    attachment_section.columnconfigure(2, weight=0)
    attachment_section.columnconfigure(3, weight=0)

    upload_button = ttk.Button(
        attachment_section,
        text="+",
        width=3,
        command=lambda: self.upload_tech_pack_attachment_page(row, detail_state),
    )
    upload_button.grid(row=0, column=3, sticky="e", padx=10, pady=(8, 6))
    upload_button.configure(state="normal" if self.is_action_allowed("action.tech_pack.detail_save") else "disabled")

    if attachments:
        for index, item in enumerate(attachments, start=0):
            title = str(item.get("title", "")).strip() or f"Attachment {index + 1}"
            page_number = str(item.get("page_number", "")).strip()
            summary = title if not page_number else f"{title} - Page {page_number}"
            source_value = _tech_pack_attachment_source_value(item)
            ttk.Label(
                attachment_section,
                text=summary,
                anchor="w",
                relief="solid",
                padding=(8, 6),
            ).grid(row=index + 1, column=0, sticky="ew", padx=10, pady=(0, 8))
            download_button = ttk.Button(
                attachment_section,
                text="Download",
                command=lambda value=source_value: self.open_tech_pack_attachment_file(value),
            )
            download_button.grid(row=index + 1, column=1, sticky="e", padx=(0, 8), pady=(0, 8))
            download_button.configure(state="normal" if source_value else "disabled")
            delete_button = ttk.Button(
                attachment_section,
                text="Delete",
                command=lambda item_index=index: self.delete_tech_pack_attachment_page(row, item_index, detail_state),
            )
            delete_button.grid(row=index + 1, column=2, sticky="e", padx=(0, 10), pady=(0, 8))
            delete_button.configure(state="normal" if self.is_action_allowed("action.tech_pack.detail_save") else "disabled")
    else:
        ttk.Label(
            attachment_section,
            text="Khong tim thay attachment page nao trong Tech Pack nay.",
            foreground="#7a8698",
        ).grid(row=1, column=0, sticky="w", padx=10, pady=(0, 10))

def build_tech_pack_more_tab(self, parent: ttk.Frame, row: dict, detail_state: dict) -> None:
    parent.columnconfigure(0, weight=1)
    host = ttk.Frame(parent)
    host.pack(fill="both", expand=True)
    host.columnconfigure(0, weight=1)
    host.columnconfigure(1, weight=1)
    self.build_tech_pack_detail_structured_box(host, title="Production Status", value=str(row.get("production_status", "")).strip(), row_index=0, column_index=0, detail_state=detail_state, field_key="record::0::production_status")
    self.build_tech_pack_detail_structured_box(host, title="Quality Status", value=str(row.get("quality_status", "")).strip(), row_index=0, column_index=1, detail_state=detail_state, field_key="record::0::quality_status")
    self.build_tech_pack_detail_structured_box(host, title="Last Updated", value=str(row.get("last_updated", "")).strip(), row_index=1, column_index=0, detail_state=detail_state, field_key="record::0::last_updated")
    self.build_tech_pack_detail_structured_box(host, title="Overview Notes", value=str(row.get("overview_notes", "")).strip(), row_index=2, column_index=0, columnspan=2, min_height=120, wraplength=1080, detail_state=detail_state, field_key="record::0::overview_notes")

def build_tech_pack_overview_tab(self, parent: ttk.Frame, row: dict, detail_state: dict) -> None:
    parent.columnconfigure(0, weight=1)
    parent.rowconfigure(1, weight=1)

    context = self.tech_pack_guess_overview_context(row)
    preview_images = [str(item).strip() for item in list(row.get("preview_images", [])) if str(item).strip()]
    if not preview_images:
        image_path = str(row.get("image_path", "")).strip()
        if image_path:
            preview_images = [image_path]
    if len(preview_images) < 2:
        preview_images = self.refresh_tech_pack_preview_assets(row, persist=False)
    preview_images = self.get_unique_tech_pack_preview_images(preview_images)
    detail_state["preview_images"] = preview_images
    cover_image_path = str(row.get("image_path", "")).strip()
    detail_state["current_preview_index"] = preview_images.index(cover_image_path) if cover_image_path in preview_images else 0
    detail_state["cover_var"] = tk.StringVar(value=cover_image_path or (preview_images[0] if preview_images else ""))

    top = ttk.Frame(parent)
    top.grid(row=0, column=0, sticky="ew", pady=(0, 14))
    top.rowconfigure(0, weight=1)
    top.columnconfigure(0, weight=2, uniform="overview_top")
    top.columnconfigure(1, weight=4, uniform="overview_top")
    top.columnconfigure(2, weight=2, uniform="overview_top")
    top.columnconfigure(3, weight=2, uniform="overview_top")

    top_height = 272

    image_frame = tk.Frame(top, bd=1, relief="solid", bg="#ffffff", width=282, height=top_height)
    image_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
    image_frame.grid_propagate(False)
    preview_host = tk.Frame(image_frame, bg="#ffffff", width=248, height=158)
    preview_host.grid(row=0, column=0, columnspan=3, sticky="nsew", padx=8, pady=(14, 10))
    preview_host.grid_propagate(False)
    main_preview = tk.Label(preview_host, bg="#ffffff", anchor="center")
    main_preview.place(relx=0.5, rely=0.5, anchor="center")
    image_frame.rowconfigure(0, weight=0)
    image_frame.columnconfigure(1, weight=1)
    main_preview.bind(
        "<Double-Button-1>",
        lambda _event: self.open_tech_pack_preview_gallery(detail_state.get("preview_images", []), detail_state.get("current_preview_index", 0)),
    )
    main_preview.bind(
        "<Button-1>",
        lambda _event: set_main_preview((detail_state.get("current_preview_index", 0) + 1) % max(1, len(detail_state.get("preview_images", [])))),
    )

    thumb_row = ttk.Frame(image_frame)
    thumb_row.grid(row=1, column=0, columnspan=3, sticky="ew", padx=8, pady=(2, 8))
    detail_state["thumb_widgets"] = []
    detail_state["thumb_frames"] = []
    detail_state["main_preview_photo_cache"] = {}
    detail_state["main_preview_last_size"] = None

    def set_main_preview(index: int, *, force: bool = False) -> None:
        image_paths = detail_state.get("preview_images", [])
        if not image_paths:
            main_preview.configure(text=self.t("tech_pack_no_image"), fg="#7a7a7a", image="")
            main_preview.image = None
            return
        safe_index = max(0, min(index, len(image_paths) - 1))
        detail_state["current_preview_index"] = safe_index
        if detail_state.get("cover_var") is not None:
            detail_state["cover_var"].set(image_paths[safe_index])
        preview_row = dict(row)
        preview_row["image_path"] = image_paths[safe_index]
        preview_host.update_idletasks()
        target_width = max(190, preview_host.winfo_width() - 10)
        target_height = max(140, preview_host.winfo_height() - 10)
        size_key = (target_width, target_height)
        if not force and detail_state.get("main_preview_last_size") == (safe_index, size_key):
            return
        detail_state["main_preview_last_size"] = (safe_index, size_key)
        cache_key = (image_paths[safe_index], target_width, target_height)
        preview_cache = detail_state.setdefault("main_preview_photo_cache", {})
        main_photo = preview_cache.get(cache_key)
        if main_photo is None:
            main_photo = self.create_tech_pack_thumbnail(preview_row, target_width=target_width, target_height=target_height)
            if main_photo is not None:
                preview_cache[cache_key] = main_photo
        if main_photo is None:
            main_preview.configure(text=self.t("tech_pack_no_image"), fg="#7a7a7a", image="")
            main_preview.image = None
        else:
            main_preview.configure(image=main_photo, text="")
            main_preview.image = main_photo
        left_nav.configure(state="normal" if len(image_paths) > 1 else "disabled")
        right_nav.configure(state="normal" if len(image_paths) > 1 else "disabled")
        for thumb_idx, thumb_widget in enumerate(detail_state.get("thumb_widgets", [])):
            thumb_widget.configure(highlightthickness=2 if thumb_idx == safe_index else 1, highlightbackground="#4f6f96" if thumb_idx == safe_index else "#d0d0d0")
        for thumb_idx, thumb_frame in enumerate(detail_state.get("thumb_frames", [])):
            thumb_frame.configure(highlightthickness=2 if thumb_idx == safe_index else 1, highlightbackground="#4f6f96" if thumb_idx == safe_index else "#d9d9d9")
    left_nav = ttk.Button(image_frame, text="<", width=2, command=lambda: set_main_preview((detail_state.get("current_preview_index", 0) - 1) % max(1, len(detail_state.get("preview_images", [])))))
    left_nav.place(x=6, rely=0.40, anchor="w")
    right_nav = ttk.Button(image_frame, text=">", width=2, command=lambda: set_main_preview((detail_state.get("current_preview_index", 0) + 1) % max(1, len(detail_state.get("preview_images", [])))))
    right_nav.place(relx=1.0, x=-6, rely=0.40, anchor="e")

    thumbs_frame = ttk.Frame(thumb_row)
    thumbs_frame.grid(row=0, column=0, sticky="w", padx=(2, 0))
    for thumb_index in range(len(preview_images)):
        thumb_card = tk.Frame(
            thumbs_frame,
            bg="#ffffff",
            relief="solid",
            bd=0,
            highlightthickness=1,
            highlightbackground="#d9d9d9",
            padx=4,
            pady=4,
        )
        thumb_card.pack(side="left", padx=(0, 6))
        thumb = tk.Label(
            thumb_card,
            bg="#ffffff",
            relief="flat",
            bd=0,
            cursor="hand2",
        )
        thumb.pack(side="top")
        preview_row = dict(row)
        preview_row["image_path"] = preview_images[thumb_index]
        thumb_photo = self.create_tech_pack_thumbnail(preview_row, target_width=42, target_height=42)
        if thumb_photo is not None:
            thumb.configure(image=thumb_photo)
            thumb.image = thumb_photo
        thumb.bind("<Button-1>", lambda _event, idx=thumb_index: set_main_preview(idx))
        cover_radio = tk.Radiobutton(
            thumb_card,
            text="",
            value=preview_images[thumb_index],
            variable=detail_state["cover_var"],
            bg="#ffffff",
            activebackground="#ffffff",
            font=("Segoe UI", 8),
            command=lambda idx=thumb_index: set_main_preview(idx),
            padx=0,
            pady=0,
            width=1,
        )
        cover_radio.pack(side="top", pady=(3, 0))
        detail_state["thumb_widgets"].append(thumb)
        detail_state["thumb_frames"].append(thumb_card)
    def refresh_main_preview_on_resize(event: tk.Event) -> None:
        current_size = (max(190, int(event.width or 0) - 10), max(140, int(event.height or 0) - 10))
        if detail_state.get("main_preview_configure_size") == current_size:
            return
        detail_state["main_preview_configure_size"] = current_size
        set_main_preview(detail_state.get("current_preview_index", 0))

    preview_host.bind("<Configure>", refresh_main_preview_on_resize)
    set_main_preview(detail_state.get("current_preview_index", 0), force=True)

    owners = ttk.Frame(top)
    owners.grid(row=0, column=1, sticky="nsew", padx=(0, 10))
    owners.columnconfigure(0, weight=1)
    owners.columnconfigure(1, weight=1)
    owners.configure(height=top_height)
    owners.grid_propagate(False)
    tk.Label(owners, text="Owners & Status", font=("Segoe UI", 15, "bold"), fg="#4a4a4a").grid(
        row=0, column=0, columnspan=2, sticky="w", pady=(2, 10)
    )
    for index, field in enumerate(context["owner_fields"]):
        col = index % 2
        row_index = index // 2
        self.build_tech_pack_overview_form_field(
            owners,
            row_index,
            col,
            field,
            width=22,
            row_offset=1,
            detail_state=detail_state,
        )

    notes_section = ttk.Frame(top)
    notes_section.grid(row=0, column=2, sticky="nsew", padx=(0, 10))
    notes_section.columnconfigure(0, weight=1)
    notes_section.configure(height=top_height)
    notes_section.grid_propagate(False)
    tk.Label(notes_section, text="Notes and Comments", font=("Segoe UI", 15, "bold"), fg="#4a4a4a").grid(
        row=0, column=0, sticky="w", pady=(2, 10)
    )
    self.build_tech_pack_overview_textarea(
        notes_section,
        title="",
        field_key="design_details",
        value=context["design_details"],
        width=26,
        height=10,
        detail_state=detail_state,
    ).grid(row=1, column=0, sticky="nsew")

    lead_section = ttk.Frame(top)
    lead_section.grid(row=0, column=3, sticky="nsew")
    for lead_col in range(3):
        lead_section.columnconfigure(lead_col, weight=1)
    lead_section.configure(height=top_height)
    lead_section.grid_propagate(False)
    tk.Label(lead_section, text="Lead Style Information", font=("Segoe UI", 15, "bold"), fg="#4a4a4a").grid(
        row=0, column=0, columnspan=3, sticky="w", pady=(2, 10)
    )
    relationship = self.get_tech_pack_relationship_context(row)
    lead_field_map = {str(item.get("key", "")).strip(): dict(item) for item in context["lead_style_fields"]}
    lead_style_field = lead_field_map.get("lead_style_no", {"key": "lead_style_no", "label": "Lead Style No", "value": ""})
    lead_season_field = lead_field_map.get("lead_season", {"key": "lead_season", "label": "Season", "value": ""})
    lead_season_year_field = lead_field_map.get("lead_season_year", {"key": "lead_season_year", "label": "Season Year", "value": ""})

    ttk.Label(lead_section, text=str(lead_style_field.get("label", "Lead Style No"))).grid(row=1, column=0, sticky="w", pady=(0, 3))
    lead_style_var = tk.StringVar(value=str(lead_style_field.get("value", "")).strip())
    lead_style_values = ["Lead", *relationship["available_style_codes"]]
    lead_style_combo = ttk.Combobox(lead_section, textvariable=lead_style_var, values=lead_style_values, state="normal", width=16)
    lead_style_combo.grid(row=2, column=0, sticky="ew", padx=(0, 10), pady=(0, 8), ipady=2)
    detail_state.setdefault("field_vars", {})["lead_style_no"] = lead_style_var
    detail_state.setdefault("field_meta", {})["lead_style_no"] = dict(lead_style_field)
    detail_state.setdefault("field_entries", []).append((lead_style_combo, False))

    def refresh_lead_style_matches(_event: object | None = None) -> None:
        query = lead_style_var.get().strip().lower()
        if not query:
            matched_values = list(lead_style_values)
        else:
            starts_with = [candidate for candidate in lead_style_values if candidate.lower().startswith(query)]
            contains = [candidate for candidate in lead_style_values if query in candidate.lower() and candidate not in starts_with]
            matched_values = starts_with + contains
        lead_style_combo.configure(values=matched_values or list(lead_style_values))

    lead_style_combo.bind("<KeyRelease>", refresh_lead_style_matches)
    lead_style_combo.bind("<FocusIn>", refresh_lead_style_matches)

    ttk.Label(lead_section, text=str(lead_season_field.get("label", "Season"))).grid(row=1, column=1, sticky="w", pady=(0, 3))
    lead_season_var = tk.StringVar(value=str(lead_season_field.get("value", "")).strip())
    lead_season_entry = tk.Entry(
        lead_section,
        textvariable=lead_season_var,
        relief="solid",
        bd=1,
        font=("Segoe UI", 10),
        readonlybackground="#ededed",
        disabledbackground="#ededed",
        fg="#333333",
        width=10,
    )
    lead_season_entry.grid(row=2, column=1, sticky="ew", padx=(0, 10), pady=(0, 8), ipady=3)
    lead_season_entry.configure(state="readonly")
    detail_state.setdefault("field_vars", {})["lead_season"] = lead_season_var
    detail_state.setdefault("field_meta", {})["lead_season"] = dict(lead_season_field)
    detail_state.setdefault("field_entries", []).append((lead_season_entry, True))

    ttk.Label(lead_section, text=str(lead_season_year_field.get("label", "Season Year"))).grid(row=1, column=2, sticky="w", pady=(0, 3))
    lead_season_year_var = tk.StringVar(value=str(lead_season_year_field.get("value", "")).strip())
    lead_season_year_entry = tk.Entry(
        lead_section,
        textvariable=lead_season_year_var,
        relief="solid",
        bd=1,
        font=("Segoe UI", 10),
        readonlybackground="#ededed",
        disabledbackground="#ededed",
        fg="#333333",
        width=10,
    )
    lead_season_year_entry.grid(row=2, column=2, sticky="ew", pady=(0, 8), ipady=3)
    lead_season_year_entry.configure(state="readonly")
    detail_state.setdefault("field_vars", {})["lead_season_year"] = lead_season_year_var
    detail_state.setdefault("field_meta", {})["lead_season_year"] = dict(lead_season_year_field)
    detail_state.setdefault("field_entries", []).append((lead_season_year_entry, True))

    def sync_lead_style_context(_event: object | None = None) -> None:
        selected_value = lead_style_var.get().strip()
        normalized_selected = normalize_style_identifier(selected_value)
        if selected_value.lower() == "lead" or normalized_selected == str(row.get("style_code", "")).strip():
            lead_style_var.set("Lead")
            lead_season_var.set(str(row.get("season", "")).strip())
            current_overview = row.get("overview_data", {}) if isinstance(row.get("overview_data", {}), dict) else {}
            lead_season_year_var.set(str(current_overview.get("season_year", "")).strip())
        elif normalized_selected:
            library_by_style = {
                str(item.get("style_code", "")).strip(): dict(item)
                for item in list(getattr(self, "tech_pack_library", []) or [])
                if str(item.get("style_code", "")).strip()
            }
            linked_record = dict(library_by_style.get(normalized_selected, {}))
            linked_overview = linked_record.get("overview_data", {}) if isinstance(linked_record.get("overview_data", {}), dict) else {}
            lead_style_var.set(normalized_selected)
            lead_season_var.set(str(linked_overview.get("season", "")).strip() or str(linked_record.get("season", "")).strip())
            lead_season_year_var.set(str(linked_overview.get("season_year", "")).strip())
        detail_state["refresh_baby_style_panel"]()

    lead_style_combo.bind("<<ComboboxSelected>>", sync_lead_style_context)

    baby_style_frame = ttk.Frame(lead_section)
    baby_style_frame.grid(row=3, column=0, columnspan=3, sticky="nsew", pady=(8, 0))
    baby_style_frame.columnconfigure(0, weight=1)
    baby_style_frame.rowconfigure(1, weight=1)
    ttk.Label(baby_style_frame, text="Baby Style").grid(row=0, column=0, sticky="w", pady=(0, 4))
    baby_style_list_host = ttk.Frame(baby_style_frame)
    baby_style_list_host.grid(row=1, column=0, sticky="nsew")
    detail_state["baby_style_list_host"] = baby_style_list_host
    detail_state["refresh_baby_style_panel"] = lambda: self.refresh_tech_pack_baby_style_panel(row, detail_state)
    detail_state["refresh_baby_style_panel"]()

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

    canvas = tk.Canvas(scroll_host, highlightthickness=0, bg="#ffffff")
    canvas.grid(row=0, column=0, sticky="nsew")
    scrollbar = ttk.Scrollbar(scroll_host, orient="vertical", command=canvas.yview)
    scrollbar.grid(row=0, column=1, sticky="ns")
    canvas.configure(yscrollcommand=scrollbar.set)
    bottom = ttk.Frame(canvas)
    bottom_window = canvas.create_window((0, 0), window=bottom, anchor="nw")

    def sync_bottom_width(event: tk.Event) -> None:
        canvas.itemconfigure(bottom_window, width=event.width)

    def update_scrollregion(_event: tk.Event) -> None:
        canvas.configure(scrollregion=canvas.bbox("all"))

    def on_mousewheel(event: tk.Event) -> str:
        canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
        return "break"

    def bind_mousewheel(_event: tk.Event) -> None:
        canvas.bind_all("<MouseWheel>", on_mousewheel)

    def unbind_mousewheel(_event: tk.Event) -> None:
        canvas.unbind_all("<MouseWheel>")

    canvas.bind("<Configure>", sync_bottom_width)
    bottom.bind("<Configure>", update_scrollregion)
    for widget in (canvas, bottom, parent):
        widget.bind("<Enter>", bind_mousewheel)
        widget.bind("<Leave>", unbind_mousewheel)

    bottom.grid_columnconfigure(0, weight=1)
    bottom.grid_columnconfigure(1, weight=1)
    bottom.grid_columnconfigure(2, weight=2)
    bottom.columnconfigure(0, weight=1)
    bottom.columnconfigure(1, weight=1)
    bottom.columnconfigure(2, weight=2)

    product_section = self.build_tech_pack_overview_section_fields(
        bottom,
        "Product Information",
        context["product_fields"],
        wide_label="Style Description",
        detail_state=detail_state,
    )
    product_section.grid(row=0, column=0, sticky="nwse", padx=(0, 14))

    hierarchy_section = self.build_tech_pack_overview_section_fields(
        bottom,
        "Hierarchy Information",
        context["hierarchy_fields"],
        detail_state=detail_state,
    )
    hierarchy_section.grid(row=0, column=1, sticky="nwse", padx=(0, 14))

    right_stack = ttk.Frame(bottom)
    right_stack.grid(row=0, column=2, sticky="nwse")
    right_stack.columnconfigure(0, weight=1)

    color_section = ttk.Frame(right_stack)
    color_section.grid(row=0, column=0, sticky="ew")
    tk.Label(color_section, text="Color Info - Customer/Choice", font=("Segoe UI", 15, "bold"), fg="#4a4a4a").grid(
        row=0, column=0, columnspan=8, sticky="w", pady=(0, 10)
    )
    color_headers = ["", "Name", "Bealls Name", "CSP Ref", "Colorway Type", "Color Code", "Status", "Size Attributes"]
    widths = [4, 26, 26, 16, 14, 12, 10, 14]
    for column_index, (header, width) in enumerate(zip(color_headers, widths, strict=False)):
        tk.Label(color_section, text=header, font=("Segoe UI", 9), fg="#6c748c").grid(
            row=1, column=column_index, sticky="w", padx=(0, 6), pady=(0, 4)
        )
    for row_index, color_row in enumerate(context["color_rows"], start=2):
        swatch = tk.Label(color_section, bg=color_row["swatch"], relief="solid", bd=1, width=3)
        swatch.grid(row=row_index, column=0, sticky="nsew", padx=(0, 6), pady=(0, 6), ipady=6)
        self.build_tech_pack_overview_table_cell(color_section, row_index, 1, color_row["name"], bg="#ededed", width=26)
        self.build_tech_pack_overview_table_cell(color_section, row_index, 2, color_row["bealls_name"], bg="#ededed", width=26)
        self.build_tech_pack_overview_table_cell(color_section, row_index, 3, color_row["csp_ref"], bg="#ededed", width=16)
        self.build_tech_pack_overview_table_cell(color_section, row_index, 4, color_row["colorway_type"], bg="#ededed", width=14)
        self.build_tech_pack_overview_table_cell(color_section, row_index, 5, color_row["color_code"], bg="#ededed", width=12)
        self.build_tech_pack_overview_table_cell(color_section, row_index, 6, color_row["status"], bg="#ededed", width=10)
        self.build_tech_pack_overview_table_cell(color_section, row_index, 7, color_row["size_attributes"], bg="#ededed", width=14)

    size_section = ttk.Frame(right_stack)
    size_section.grid(row=1, column=0, sticky="ew", pady=(18, 0))
    tk.Label(size_section, text="Size Information", font=("Segoe UI", 15, "bold"), fg="#4a4a4a").grid(
        row=0, column=0, columnspan=4, sticky="w", pady=(0, 10)
    )
    size_headers = ["Size Range", "Size Class", "Family of Business"]
    for column_index, header in enumerate(size_headers):
        tk.Label(size_section, text=header, font=("Segoe UI", 9), fg="#6c748c").grid(
            row=1, column=column_index, sticky="w", padx=(0, 6), pady=(0, 4)
        )
    for row_index, (size_range, size_class, family) in enumerate(context["size_rows"], start=2):
        self.build_tech_pack_overview_table_cell(
            size_section,
            row_index,
            0,
            size_range,
            bg="#fff34d" if size_range else "#ededed",
            width=24,
        )
        self.build_tech_pack_overview_table_cell(size_section, row_index, 1, size_class, bg="#ededed", width=14)
        self.build_tech_pack_overview_table_cell(size_section, row_index, 2, family, bg="#ededed", width=14)

    treatment_section = ttk.Frame(right_stack)
    treatment_section.grid(row=2, column=0, sticky="ew", pady=(18, 0))
    tk.Label(
        treatment_section,
        text="Finished Goods Treatments",
        font=("Segoe UI", 15, "bold"),
        fg="#4a4a4a",
    ).grid(row=0, column=0, columnspan=4, sticky="w", pady=(0, 10))
    treatment_headers = ["Treatment", "Treatment Type", "Testing Required Y/N?"]
    for column_index, header in enumerate(treatment_headers):
        tk.Label(treatment_section, text=header, font=("Segoe UI", 9), fg="#6c748c").grid(
            row=1, column=column_index, sticky="w", padx=(0, 6), pady=(0, 4)
        )
    for row_index, (treatment, treatment_type, testing) in enumerate(context["treatment_rows"], start=2):
        self.build_tech_pack_overview_table_cell(treatment_section, row_index, 0, treatment, bg="#ededed", width=32)
        self.build_tech_pack_overview_table_cell(treatment_section, row_index, 1, treatment_type, bg="#ededed", width=16)
        self.build_tech_pack_overview_table_cell(treatment_section, row_index, 2, testing, bg="#ededed", width=14)

def open_tech_pack_detail_window(self, row: dict) -> None:
    style_code = self.display_value(row.get("style_code"))
    normalized_style_code = normalize_style_identifier(style_code)
    if normalized_style_code:
        full_record = next(
            (
                dict(item)
                for item in list(getattr(self, "tech_pack_library", []) or [])
                if normalize_style_identifier(item.get("style_code")) == normalized_style_code
            ),
            None,
        )
        if isinstance(full_record, dict) and full_record:
            merged_row = dict(row)
            merged_row.update(full_record)
            row = merged_row
    style_identity = str(row.get("style_identity", "")).strip()
    if not style_identity and hasattr(self, "build_style_identity"):
        overview_data = row.get("overview_data", {}) if isinstance(row.get("overview_data", {}), dict) else {}
        style_identity = self.build_style_identity(
            row.get("style_code"),
            overview_data.get("season") or row.get("season_base") or row.get("season"),
            overview_data.get("season_year") or row.get("season_year"),
        )
    detail_title = f"{self.t('tech_pack_detail_title')} - {style_identity or style_code}"
    cache_key = normalize_style_identifier(row.get("style_code")) or str(style_code or "").strip()
    current_record_updated_at = str(row.get("updated_at", "") or row.get("last_updated", "") or "").strip()
    if cache_key:
        window_cache = getattr(self, "tech_pack_detail_window_cache", None)
        if not isinstance(window_cache, dict):
            window_cache = {}
            self.tech_pack_detail_window_cache = window_cache
        cached_dialog = window_cache.get(cache_key)
        if cached_dialog is not None:
            try:
                if int(cached_dialog.winfo_exists()) == 1:
                    cached_updated_at = str(getattr(cached_dialog, "_tech_pack_record_updated_at", "") or "").strip()
                    if current_record_updated_at and cached_updated_at != current_record_updated_at:
                        cached_dialog.destroy()
                        window_cache.pop(cache_key, None)
                    else:
                        cached_dialog.deiconify()
                        cached_dialog.lift()
                        cached_dialog.focus_force()
                        return
                else:
                    window_cache.pop(cache_key, None)
            except Exception:
                window_cache.pop(cache_key, None)
    debug_log_path = DEBUG_DIR / "tech_pack_detail_debug.log"

    def log_detail_debug(message: str) -> None:
        try:
            DEBUG_DIR.mkdir(parents=True, exist_ok=True)
            with debug_log_path.open("a", encoding="utf-8") as handle:
                handle.write(f"[{datetime.now().isoformat(timespec='seconds')}] {message}\n")
        except Exception:
            pass

    dialog = tk.Toplevel(self.root)
    try:
        dialog._tech_pack_record_updated_at = current_record_updated_at
    except Exception:
        pass
    if cache_key:
        self.tech_pack_detail_window_cache[cache_key] = dialog
        def drop_cached_tech_pack_detail_window(event: tk.Event, key: str = cache_key) -> None:
            if event.widget is dialog:
                getattr(self, "tech_pack_detail_window_cache", {}).pop(key, None)

        dialog.bind("<Destroy>", drop_cached_tech_pack_detail_window, add="+")
    _apply_window_icon(dialog)
    try:
        log_detail_debug(f"open start style={style_code}")
        dialog.title(detail_title)
        dialog.resizable(True, True)
        dialog.minsize(1100, 760)
        detail_geometry = str(self.ui_state.get("tech_pack_detail_geometry", "")).strip()
        if detail_geometry:
            dialog.geometry(detail_geometry)
        else:
            dialog.geometry("1400x900")
        if str(self.ui_state.get("tech_pack_detail_state", "")).strip() == "zoomed":
            try:
                dialog.state("zoomed")
            except Exception:
                pass

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

        header = ttk.Frame(frame)
        header.grid(row=0, column=0, sticky="ew")
        header.columnconfigure(0, weight=1)
        ttk.Label(header, text=detail_title, style="Section.TLabel").grid(
            row=0, column=0, sticky="w"
        )
        detail_state: dict = {
            "edit_mode": False,
            "detail_window": dialog,
            "field_vars": {},
            "field_meta": {},
            "field_entries": [],
            "text_entries": [],
            "detail_tab_widgets": {},
            "bom_widgets": {},
        }
        controls = ttk.Frame(header)
        controls.grid(row=0, column=1, sticky="e")
        export_detail_button = ttk.Button(controls, text="Export TP", command=lambda: self.export_tech_pack_detail_excel(row, detail_state))
        export_detail_button.pack(side="left", padx=(0, 6))
        edit_detail_button = ttk.Button(controls, text="Edit", command=lambda: self.toggle_tech_pack_detail_edit_mode(detail_state, True))
        edit_detail_button.pack(side="left", padx=(0, 6))
        save_detail_button = ttk.Button(controls, text="Save", command=lambda: self.save_tech_pack_detail_overview(row, detail_state))
        save_detail_button.pack(side="left")
        detail_state["save_button"] = save_detail_button
        self.register_permission_widget("action.tech_pack.export_report", export_detail_button)
        self.register_permission_widget("action.tech_pack.detail_edit", edit_detail_button)
        self.register_permission_widget("action.tech_pack.detail_save", save_detail_button)
        export_detail_button.configure(state="normal" if self.is_action_allowed("action.tech_pack.export_report") else "disabled")
        edit_detail_button.configure(state="normal" if self.is_action_allowed("action.tech_pack.detail_edit") else "disabled")
        save_detail_button.configure(state="normal" if self.is_action_allowed("action.tech_pack.detail_save") else "disabled")

        loading_card = tk.Frame(frame, bg="#ffffff", bd=1, relief="solid")
        loading_card.grid(row=1, column=0, sticky="nsew", pady=(12, 0))
        loading_card.columnconfigure(0, weight=1)
        loading_card.rowconfigure(1, weight=1)
        tk.Label(
            loading_card,
            text="Loading Tech Pack Detail...",
            bg="#ffffff",
            fg="#333333",
            font=("Segoe UI", 11, "bold"),
        ).grid(row=0, column=0, sticky="w", padx=14, pady=(14, 6))
        loading_text = tk.Text(
            loading_card,
            wrap="word",
            relief="flat",
            bg="#ffffff",
            fg="#666666",
            font=("Consolas", 9),
            height=8,
        )
        loading_text.grid(row=1, column=0, sticky="nsew", padx=14, pady=(0, 14))
        loading_text.insert("1.0", f"Preparing detail window for style {style_code}...\n")
        loading_text.configure(state="disabled")
        dialog.update_idletasks()

        def append_loading(message: str) -> None:
            log_detail_debug(message)
            try:
                loading_text.configure(state="normal")
                loading_text.insert("end", message + "\n")
                loading_text.see("end")
                loading_text.configure(state="disabled")
                dialog.update_idletasks()
            except Exception:
                pass

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

        tabs: dict[str, ttk.Frame] = {}
        detail_state["tab_ids"] = {}
        for key, text_key in TECH_PACK_DETAIL_TABS:
            tab = ttk.Frame(notebook, padding=14)
            tabs[key] = tab
            notebook.add(tab, text=self.t(text_key))
            detail_state["tab_ids"][str(tab)] = key
        append_loading("Notebook created.")
        tab_builders = {
            "overview": self.build_tech_pack_overview_tab,
            "bom": self.build_tech_pack_bom_tab,
            "constructions": self.build_tech_pack_constructions_tab,
            "instructions": self.build_tech_pack_instructions_tab,
            "stitching": self.build_tech_pack_stitching_tab,
            "pom": self.build_tech_pack_pom_tab,
            "consumption": self.build_tech_pack_consumption_tab,
            "samples": self.build_tech_pack_samples_tab,
            "attachments": self.build_tech_pack_attachments_tab,
            "more": self.build_tech_pack_more_tab,
        }
        rendered_tabs: set[str] = set()
        detail_state["rendered_tabs"] = rendered_tabs

        def render_tech_pack_tab(tab_key: str, builder) -> None:
            if tab_key in rendered_tabs:
                return
            rendered_tabs.add(tab_key)
            tab = tabs[tab_key]
            append_loading(f"Rendering tab: {tab_key}")
            try:
                effective_row = self.get_tech_pack_effective_detail_row(row, tab_key)
                host = ttk.Frame(tab)
                host.pack(fill="both", expand=True)
                inherited_from_style = str(effective_row.get("_inherited_from_style", "")).strip()
                if inherited_from_style:
                    banner = ttk.Frame(host)
                    banner.pack(fill="x", pady=(0, 10))
                    ttk.Label(
                        banner,
                        text=f"Inherited from lead style {inherited_from_style}. Edit the lead style to update this tab.",
                        foreground="#6c748c",
                    ).pack(anchor="w")
                    content = ttk.Frame(host)
                    content.pack(fill="both", expand=True)
                    builder(content, effective_row, detail_state)
                else:
                    builder(host, effective_row, detail_state)
                append_loading(f"Rendered tab: {tab_key}")
            except Exception as exc:
                traceback.print_exc()
                log_detail_debug(traceback.format_exc())
                fallback = ttk.Frame(tab)
                fallback.pack(fill="both", expand=True)
                fallback.columnconfigure(0, weight=1)
                fallback.rowconfigure(1, weight=1)
                ttk.Label(
                    fallback,
                    text=f"Failed to render tab '{tab_key}'",
                    foreground="#a1260d",
                    font=("Segoe UI", 10, "bold"),
                ).grid(row=0, column=0, sticky="w", pady=(0, 8))
                error_box = tk.Text(
                    fallback,
                    wrap="word",
                    relief="solid",
                    bd=1,
                    font=("Consolas", 9),
                    bg="#fff7f6",
                    fg="#7a1f10",
                    padx=6,
                    pady=6,
                )
                error_box.grid(row=1, column=0, sticky="nsew")
                error_box.insert("1.0", f"{type(exc).__name__}: {exc}\n\n{traceback.format_exc()}")
                error_box.configure(state="disabled")

        def on_close_detail() -> None:
            try:
                self.ui_state["tech_pack_detail_state"] = str(dialog.state())
            except Exception:
                self.ui_state["tech_pack_detail_state"] = "normal"
            try:
                self.ui_state["tech_pack_detail_geometry"] = dialog.winfo_geometry()
            except Exception:
                pass
            self.persist_ui_state()
            try:
                dialog.withdraw()
            except Exception:
                if cache_key:
                    getattr(self, "tech_pack_detail_window_cache", {}).pop(cache_key, None)
                dialog.destroy()

        dialog.protocol("WM_DELETE_WINDOW", on_close_detail)

        render_tech_pack_tab("overview", tab_builders["overview"])
        loading_card.destroy()
        notebook.grid()
        try:
            notebook.select(tabs["overview"])
        except Exception:
            pass
        dialog.update_idletasks()
        log_detail_debug("Tech Pack Detail overview ready; remaining tabs render on first select and stay cached.")

        def render_selected_tab_once(_event: tk.Event | None = None) -> None:
            try:
                if int(dialog.winfo_exists()) != 1:
                    return
            except Exception:
                return
            tab_key = self.get_current_tech_pack_detail_tab_key(detail_state)
            if not tab_key or tab_key in rendered_tabs:
                return
            builder = tab_builders.get(tab_key)
            if builder is not None:
                render_tech_pack_tab(tab_key, builder)

        notebook.bind("<<NotebookTabChanged>>", render_selected_tab_once, add="+")
    except Exception as exc:
        if cache_key:
            getattr(self, "tech_pack_detail_window_cache", {}).pop(cache_key, None)
        traceback.print_exc()
        log_detail_debug(traceback.format_exc())
        for child in dialog.winfo_children():
            try:
                child.destroy()
            except Exception:
                pass
        fallback = ttk.Frame(dialog, padding=16)
        fallback.pack(fill="both", expand=True)
        fallback.columnconfigure(0, weight=1)
        fallback.rowconfigure(1, weight=1)
        ttk.Label(
            fallback,
            text="Failed to open Tech Pack Detail",
            foreground="#a1260d",
            font=("Segoe UI", 11, "bold"),
        ).grid(row=0, column=0, sticky="w", pady=(0, 8))
        error_box = tk.Text(
            fallback,
            wrap="word",
            relief="solid",
            bd=1,
            font=("Consolas", 9),
            bg="#fff7f6",
            fg="#7a1f10",
            padx=6,
            pady=6,
        )
        error_box.grid(row=1, column=0, sticky="nsew")
        error_box.insert("1.0", f"{type(exc).__name__}: {exc}\n\n{traceback.format_exc()}\n\nLog: {debug_log_path}")
        error_box.configure(state="disabled")



def bind_tech_pack_detail_module(app_cls: type) -> None:
    method_map = {
        "tech_pack_guess_overview_context": tech_pack_guess_overview_context,
        "build_tech_pack_overview_form_field": build_tech_pack_overview_form_field,
        "build_tech_pack_overview_textarea": build_tech_pack_overview_textarea,
        "build_tech_pack_overview_section_fields": build_tech_pack_overview_section_fields,
        "build_tech_pack_overview_table_cell": build_tech_pack_overview_table_cell,
        "build_tech_pack_bom_grid_cell": build_tech_pack_bom_grid_cell,
        "get_tech_pack_color_lookup": get_tech_pack_color_lookup,
        "build_tech_pack_bom_color_token_set": build_tech_pack_bom_color_token_set,
        "resolve_tech_pack_bom_thumbnail_path": resolve_tech_pack_bom_thumbnail_path,
        "resolve_tech_pack_bom_item_thumbnail_path": resolve_tech_pack_bom_item_thumbnail_path,
        "strip_tech_pack_bom_color_noise": strip_tech_pack_bom_color_noise,
        "split_tech_pack_bom_repeated_prefix": split_tech_pack_bom_repeated_prefix,
        "dedupe_repeated_tokens": dedupe_repeated_tokens,
        "build_tech_pack_artwork_colorways": build_tech_pack_artwork_colorways,
        "get_tech_pack_bom_item_override": get_tech_pack_bom_item_override,
        "get_tech_pack_bom_display_values": get_tech_pack_bom_display_values,
        "get_tech_pack_bom_color_secondary_line": get_tech_pack_bom_color_secondary_line,
        "get_tech_pack_bom_color_primary_line": get_tech_pack_bom_color_primary_line,
        "get_tech_pack_bom_color_override": get_tech_pack_bom_color_override,
        "tech_pack_bom_item_has_colorway_data": tech_pack_bom_item_has_colorway_data,
        "build_tech_pack_bom_color_rows": build_tech_pack_bom_color_rows,
        "should_show_tech_pack_bom_item_image": should_show_tech_pack_bom_item_image,
        "build_tech_pack_bom_detail_box": build_tech_pack_bom_detail_box,
        "build_tech_pack_bom_color_box": build_tech_pack_bom_color_box,
        "create_tech_pack_bom_preview_photo": create_tech_pack_bom_preview_photo,
        "build_tech_pack_bom_tab": build_tech_pack_bom_tab,
        "open_tech_pack_preview_gallery": open_tech_pack_preview_gallery,
        "toggle_tech_pack_detail_edit_mode": toggle_tech_pack_detail_edit_mode,
        "get_current_tech_pack_detail_tab_key": get_current_tech_pack_detail_tab_key,
        "build_tech_pack_detail_patch": build_tech_pack_detail_patch,
        "save_tech_pack_detail_overview": save_tech_pack_detail_overview,
        "get_tech_pack_detail_tab_override": get_tech_pack_detail_tab_override,
        "build_tech_pack_detail_structured_box": build_tech_pack_detail_structured_box,
        "build_tech_pack_detail_table_cell": build_tech_pack_detail_table_cell,
        "build_tech_pack_constructions_tab": build_tech_pack_constructions_tab,
        "build_tech_pack_stitching_tab": build_tech_pack_stitching_tab,
        "_is_tech_pack_pom_numeric_token": _is_tech_pack_pom_numeric_token,
        "_split_tech_pack_pom_value_tokens": _split_tech_pack_pom_value_tokens,
        "_is_probably_tech_pack_pom_measurement": _is_probably_tech_pack_pom_measurement,
        "_parse_tech_pack_pom_numeric_row": _parse_tech_pack_pom_numeric_row,
        "_looks_like_tech_pack_pom_row_start": _looks_like_tech_pack_pom_row_start,
        "_parse_tech_pack_pom_row_header": _parse_tech_pack_pom_row_header,
        "_tech_pack_pom_rows_need_reparse": _tech_pack_pom_rows_need_reparse,
        "extract_tech_pack_pom_rows": extract_tech_pack_pom_rows,
        "open_tech_pack_attachment_file": open_tech_pack_attachment_file,
        "delete_tech_pack_source_file": delete_tech_pack_source_file,
        "upload_tech_pack_attachment_page": upload_tech_pack_attachment_page,
        "delete_tech_pack_attachment_page": delete_tech_pack_attachment_page,
        "append_tech_pack_detail_ui_debug_log": append_tech_pack_detail_ui_debug_log,
        "get_tech_pack_relationship_context": get_tech_pack_relationship_context,
        "upsert_tech_pack_detail_library_record": upsert_tech_pack_detail_library_record,
        "get_tech_pack_effective_detail_row": get_tech_pack_effective_detail_row,
        "open_add_tech_pack_baby_style_dialog": open_add_tech_pack_baby_style_dialog,
        "remove_tech_pack_baby_style_link": remove_tech_pack_baby_style_link,
        "invoke_remove_tech_pack_baby_style_link": invoke_remove_tech_pack_baby_style_link,
        "refresh_tech_pack_baby_style_panel": refresh_tech_pack_baby_style_panel,
        "build_tech_pack_instructions_tab": build_tech_pack_instructions_tab,
        "build_tech_pack_pom_tab": build_tech_pack_pom_tab,
        "sync_bom_purchase_uom_from_consumption_versions": sync_bom_purchase_uom_from_consumption_versions,
        "get_tech_pack_consumption_versions": get_tech_pack_consumption_versions,
        "get_tech_pack_consumption_bom_candidates": get_tech_pack_consumption_bom_candidates,
        "open_select_tech_pack_consumption_bom_item_dialog": open_select_tech_pack_consumption_bom_item_dialog,
        "build_tech_pack_consumption_tab": build_tech_pack_consumption_tab,
        "get_tech_pack_linked_samples": get_tech_pack_linked_samples,
        "build_tech_pack_samples_tab": build_tech_pack_samples_tab,
        "build_tech_pack_attachments_tab": build_tech_pack_attachments_tab,
        "build_tech_pack_more_tab": build_tech_pack_more_tab,
        "build_tech_pack_overview_tab": build_tech_pack_overview_tab,
        "open_tech_pack_detail_window": open_tech_pack_detail_window,
    }
    for method_name, function in method_map.items():
        setattr(app_cls, method_name, function)
