"""Tribeca / Port & Bell catalog SKU parsing — master catalog is the single source of truth.

Data flow (normal):
  master_product  →  channel push applies prefix rules  →  Shopify Tribeca SKUs (HD-CW-…)

Upload special case (reverse):
  ingest references Tribeca render or channel SKUs (CW-…, HD-CW-…)  →  normalize to master  →  store/link

Layers (build_plytix_csv.py origin):
1. **Master** — canonical ``master_product`` SKUs (HPW-B15, ASW-BBC39/42L/R)
2. **Render filenames** — CW-B15 → HPW-B15 via ``IMAGE_RENDER_PREFIX_TO_MASTER_STYLES``
3. **Channel** — HPW-B15 → HD-CW-B15 via ``db.channel_sku_prefix_mapping`` (reverse for uploads)
"""
from __future__ import annotations

import re
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple

# From build_plytix_csv.py IMAGE_PREFIX_TO_STYLES — render zip folder prefix → master style code(s).
IMAGE_RENDER_PREFIX_TO_MASTER_STYLES: Dict[str, List[str]] = {
    "CA": ["ACH"],
    "CH": ["ACH"],
    "CBO": ["MBRO"],
    "CW": ["HPW"],
    "EB": ["EOB"],
    "OB": ["EOB"],
    "HS": ["ASG"],
    "ASW": ["ASW"],
    "WRO": ["ANRO", "MNRO"],
}

LEGACY_MODIFIER_PREFIXES = frozenset({"ASM", "DNU"})
LEGACY_SECOND_SEGMENT_STYLE_ALIASES: Dict[str, List[str]] = {
    "CH": ["ACH"],
    "BRO": ["MBRO"],
    "NRO": ["ANRO"],
    "OB": ["EOB"],
    "PW": ["HPW"],
    "SG": ["ASG"],
    "SW": ["ASW"],
}

TB_STANDARD_LINE_CONTEXT_RE = re.compile(
    r"TB[-\s]*STANDARD\s+LINE[-\s]*(?P<line>[A-Z ]+?)\s*\((?P<suffix>[A-Z]+)\)",
    re.IGNORECASE,
)

TB_STANDARD_LINE_PREFIX_TO_MASTER_STYLE: Dict[str, str] = {
    "HD-CW": "HPW",
    "HD-SNW": "ASW",
    "HD-HS": "ASG",
    "HD-WRO": "ANRO",
    "HD-CA": "ACH",
    "SH-SNW": "ESW",
    "SH-EB": "EOB",
    "STH-SNW": "MSW",
    "STH-WRO": "MNRO",
    "STH-CBO": "MBRO",
    "AM-SNW": "GSW",
}

TB_STANDARD_LINE_NAME_TO_PREFIX: Dict[str, str] = {
    "HD": "HD",
    "HOME DECOR": "HD",
    "SOHO": "SH",
    "SH": "SH",
    "SOUTHAMPTON": "STH",
    "SOUTH HAMPTON": "STH",
    "STH": "STH",
    "AM": "AM",
    "AMERICAN": "AM",
}

# Master style codes (STYLE_TIER keys in build_plytix_csv.py) — used as SKU leading segment.
MASTER_STYLE_CODES = frozenset(
    {
        "HPW",
        "ASW",
        "ESW",
        "ASG",
        "ACH",
        "EOB",
        "MSW",
        "ANRO",
        "MNRO",
        "MBRO",
    "GSW",
        "HEM",
        # Castle Craft / Fabuwood master SKU families
        "BDW",
        "BTP",
        "CGO",
        "CSG",
        "FGH",
        "FHF",
    }
)

MASTER_RANGE_RE = re.compile(
    r"^/(?P<high>\d+)(?P<variant>[A-Z]*)(?:/(?P<rest>.+))?$",
    re.IGNORECASE,
)
# Master raw item with embedded slash range: BBC39/42L/R, BEC1120/1124L/R, FPP96/48/R
MASTER_SLASH_ITEM_RE = re.compile(
    r"^([A-Z]+)(?P<low>\d+)/(?P<high>\d+)(?P<variant>[A-Z]*)(?:/(?P<rest>.+))?$",
    re.IGNORECASE,
)
# Collection lifestyle assets from build_plytix_csv load_vignettes()
COLLECTION_ASSET_RE = re.compile(
    r"^(?P<collection>[A-Za-z]+?)(?:_(?:Vignette|Door)|(?:Vignette|Door))",
    re.IGNORECASE,
)
# STYLE_TIER collection display name -> master style code (build_plytix_csv.py)
STYLE_COLLECTION_TO_CODE: Dict[str, str] = {
    "harrietpearlwhite": "HPW",
    "annasnowwhite": "ASW",
    "eleanorsnowwhite": "ESW",
    "annastonegray": "ASG",
    "annacaramelharvest": "ACH",
    "eleanoroceanblue": "EOB",
    "mayasnowwhite": "MSW",
    "annanaturalriftwhiteoak": "ANRO",
    "mayanaturalriftwhiteoak": "MNRO",
    "mayablackriftwhiteoak": "MBRO",
    "gracesnowwhite": "GSW",
    "hurstonespressomaple": "HEM",
    # Castle Craft / Fabuwood collection lifestyle assets (door + vignettes)
    "belmontdriftwhite": "BDW",
    "belmonttoffeepecan": "BTP",
    "chesapeakegoldenoak": "CGO",
    "chesapeakgoldenoak": "CGO",  # typo in some asset filenames
    "cooperslategray": "CSG",
    "galaxyhorizon": "FGH",
    "hallmarkfrost": "FHF",
}

_COLLECTION_KEY_DISPLAY: Dict[str, str] = {
    "harrietpearlwhite": "Harriet Pearl White",
    "annasnowwhite": "Anna Snow White",
    "eleanorsnowwhite": "Eleanor Snow White",
    "annastonegray": "Anna Stone Gray",
    "annacaramelharvest": "Anna Caramel Harvest",
    "eleanoroceanblue": "Eleanor Ocean Blue",
    "mayasnowwhite": "Maya Snow White",
    "annanaturalriftwhiteoak": "Anna Natural Rift White Oak",
    "mayanaturalriftwhiteoak": "Maya Natural Rift White Oak",
    "mayablackriftwhiteoak": "Maya Black Rift White Oak",
    "gracesnowwhite": "Grace Snow White",
    "hurstonespressomaple": "Hurston Espresso Maple",
    "belmontdriftwhite": "Belmont Drift White",
    "belmonttoffeepecan": "Belmont Toffee Pecan",
    "chesapeakegoldenoak": "Chesapeake Golden Oak",
    "chesapeakgoldenoak": "Chesapeake Golden Oak",
    "cooperslategray": "Cooper Slate Gray",
    "galaxyhorizon": "Galaxy Horizon",
    "hallmarkfrost": "Hallmark Frost",
}

MASTER_STYLE_CODE_TO_COLLECTION: Dict[str, str] = {}
for _key, _code in STYLE_COLLECTION_TO_CODE.items():
    MASTER_STYLE_CODE_TO_COLLECTION.setdefault(_code.upper(), _COLLECTION_KEY_DISPLAY[_key])


def collection_name_for_style_code(code: Optional[str]) -> Optional[str]:
    """Map master Item Style Prefix / SKU style code to collection display name."""
    text = str(code or "").strip().upper()
    if not text or text == "-":
        return None
    return MASTER_STYLE_CODE_TO_COLLECTION.get(text)


def collection_name_from_sku(sku: Optional[str]) -> Optional[str]:
    """Resolve canonical collection display name from master or channel SKU segments."""
    text = str(sku or "").strip().upper()
    if not text:
        return None

    best: Optional[str] = None

    def _consider(code: str) -> None:
        nonlocal best
        for style_code in master_style_codes_for_image_prefix(code, context=text):
            name = collection_name_for_style_code(style_code)
            if name and (not best or len(name) > len(best)):
                best = name
        name = collection_name_for_style_code(code)
        if name and (not best or len(name) > len(best)):
            best = name

    style, _ = split_styled_sku(text)
    if style:
        _consider(style)

    for segment in [part for part in text.split("-") if part][:4]:
        _consider(segment)

    return best


# Master catalog compact hand pair: BCOR33L/R, DWR3L/R (distinct from BBC39/42L/R range syntax)
MASTER_HAND_SLASH_RE = re.compile(
    r"^([A-Z]+)(?P<width>\d+)(?P<left>[LR])/(?P<right>[LR])$",
    re.IGNORECASE,
)
# Compact image item codes with 3-4 digit widths: BEC1124L, FPP9648
IMAGE_LONG_ITEM_RE = re.compile(r"^([A-Z]+)(\d{3,4})([A-Z]+)?$", re.IGNORECASE)
# Glass door renders: GD-W1530 (15x30), corner variants GD-WCOR2415 -> master GDWCOR2415
GD_W_GLASS_ITEM_RE = re.compile(r"^GD-W(\d{3,4})$", re.IGNORECASE)
GD_W_CORNER_ITEM_RE = re.compile(r"^GD-W(COR|DC)(\d{3,4})$", re.IGNORECASE)
OVEN_DOUBLE_DIAGRAM_RE = re.compile(r"^OC(\d{2})(\d{2})D$", re.IGNORECASE)
ROLLOUT_TRAY_ITEM_RE = re.compile(r"^ROTD-(\d{2})$", re.IGNORECASE)


def parse_sku(raw: str) -> Tuple[Optional[str], Optional[int], str]:
    """Return (type_code, width_int, remainder_str) — same rules as build_plytix_csv.py."""
    m = re.match(r"^([A-Z]+)(\d+)(.*)$", str(raw or "").upper())
    if not m:
        return (None, None, "")
    type_code, digits, suffix = m.groups()
    if len(digits) >= 2:
        width = int(digits[:2])
        remainder = digits[2:] + suffix
    else:
        width = int(digits)
        remainder = suffix
    return (type_code, width, remainder)


def split_styled_sku(full_sku: str) -> Tuple[str, str]:
    """Split ASW-BBC39/42L/R -> (ASW, BBC39/42L/R)."""
    text = str(full_sku or "").strip().upper()
    if "-" not in text:
        return "", text
    style, raw = text.split("-", 1)
    return style, raw


def _contextual_render_prefix_styles(prefix: str, context: Optional[str]) -> Optional[List[str]]:
    text = str(context or "").upper()
    code = str(prefix or "").strip().upper()
    if not code or not text:
        return None
    hidden_prefix = infer_tb_standard_line_prefix(text)
    if not hidden_prefix:
        for candidate_prefix in TB_STANDARD_LINE_PREFIX_TO_MASTER_STYLE:
            if candidate_prefix in text:
                hidden_prefix = candidate_prefix
                break
    if hidden_prefix:
        master_style = TB_STANDARD_LINE_PREFIX_TO_MASTER_STYLE.get(hidden_prefix)
        if master_style and hidden_prefix.rsplit("-", 1)[-1] == code:
            return [master_style]
    return None


def infer_tb_standard_line_prefix(context: Optional[str]) -> Optional[str]:
    text = str(context or "").strip().upper()
    if not text:
        return None
    match = TB_STANDARD_LINE_CONTEXT_RE.search(text)
    if not match:
        return None
    line_name = re.sub(r"\s+", " ", match.group("line").strip().upper())
    suffix = match.group("suffix").strip().upper()
    line_prefix = TB_STANDARD_LINE_NAME_TO_PREFIX.get(line_name)
    if not line_prefix:
        return None
    return f"{line_prefix}-{suffix}"


def master_style_codes_for_image_prefix(image_prefix: str, *, context: Optional[str] = None) -> List[str]:
    """Resolve render or master prefix to master style code(s) for catalog lookup."""
    prefix = str(image_prefix or "").strip().upper()
    if not prefix:
        return []
    contextual = _contextual_render_prefix_styles(prefix, context)
    if contextual is not None:
        return [code.upper() for code in contextual]
    if prefix == "SNW":
        return []
    mapped = IMAGE_RENDER_PREFIX_TO_MASTER_STYLES.get(prefix)
    if mapped:
        return [code.upper() for code in mapped]
    return [prefix]


def expand_image_key_master_styles(image_key: str) -> List[str]:
    """Expand CW-B15 -> [HPW-B15] plus original key when render prefix differs from master."""
    style, item = split_styled_sku(image_key)
    if not style:
        return [image_key.upper()]
    keys = [image_key.upper()]
    for master_style in master_style_codes_for_image_prefix(style, context=image_key):
        if master_style != style:
            keys.append(f"{master_style}-{item}")
    return keys


def apply_channel_prefix_reverse(
    reference: str,
    rules: Sequence[Tuple[str, str]],
) -> str:
    """Channel SKU → master SKU using prefix rules (RTA- aware; longest channel prefix first)."""
    from db.channel_sku_prefix_mapping import apply_prefix_rules_reverse

    text = str(reference or "").strip().upper()
    if not text or not rules:
        return text
    return apply_prefix_rules_reverse(text, rules).upper()


def expand_upload_reference_to_master_keys(
    reference: str,
    *,
    channel_prefix_rules: Optional[Sequence[Tuple[str, str]]] = None,
    context: Optional[str] = None,
) -> List[str]:
    """Expand Tribeca/channel upload references to master SKU key candidates (reverse matching)."""
    text = str(reference or "").strip().upper()
    if not text:
        return []
    ordered: List[str] = []
    seen: Set[str] = set()

    def add(key: str) -> None:
        value = str(key or "").strip().upper()
        if value and value not in seen:
            seen.add(value)
            ordered.append(value)

    add(text)
    for key in expand_legacy_modifier_reference_keys(text, context=context or text):
        add(key)
    style, item = split_styled_sku(text)
    if style and item:
        for master_style in master_style_codes_for_image_prefix(style, context=context or text):
            if master_style != style:
                add(f"{master_style}-{item}")
    else:
        for key in expand_image_key_master_styles(text):
            add(key)
    if channel_prefix_rules:
        reversed_key = apply_channel_prefix_reverse(text, channel_prefix_rules)
        add(reversed_key)
        for key in expand_image_key_master_styles(reversed_key):
            add(key)
    return ordered


def expand_legacy_modifier_reference_keys(
    reference: str,
    *,
    context: Optional[str] = None,
) -> List[str]:
    """Normalize legacy leading modifiers like ASM-CH-* or DNU-ASG-* to canonical styled keys."""
    text = str(reference or "").strip().upper()
    if not text:
        return []
    parts = [part for part in text.split("-") if part]
    if len(parts) < 3 or parts[0] not in LEGACY_MODIFIER_PREFIXES:
        return []

    style_token = parts[1]
    item = "-".join(parts[2:])
    if not item:
        return []

    styles = LEGACY_SECOND_SEGMENT_STYLE_ALIASES.get(style_token)
    if not styles:
        styles = master_style_codes_for_image_prefix(style_token, context=context or text)
    ordered: List[str] = []
    seen: Set[str] = set()
    for style in styles or []:
        key = f"{style.upper()}-{item}"
        if key not in seen:
            seen.add(key)
            ordered.append(key)
    return ordered


def _normalize_collection_name(name: str) -> str:
    return re.sub(r"[^a-z0-9]", "", str(name or "").lower())


def canonical_collection_key(name: Optional[str]) -> str:
    """Normalize collection labels and collapse known legacy / polluted aliases."""
    key = _normalize_collection_name(name or "")
    alias_map = {
        "hurstonmidnightespresso": "hurstonespressomaple",
        "chesapeakgoldenoak": "chesapeakegoldenoak",
    }
    return alias_map.get(key, key)


def style_code_for_collection_asset(file_name: str) -> Optional[str]:
    """Map AnnaSnowWhite_Vignette1a.jpg style assets to master style code (ASW)."""
    stem = str(file_name or "").rsplit(".", 1)[0]
    match = COLLECTION_ASSET_RE.match(stem)
    if not match:
        return None
    return STYLE_COLLECTION_TO_CODE.get(canonical_collection_key(match.group("collection")))


def collection_asset_key(file_name: str) -> Optional[str]:
    """Normalized collection token from vignette/door filename (e.g. hurstonespressomaple)."""
    stem = str(file_name or "").rsplit(".", 1)[0]
    match = COLLECTION_ASSET_RE.match(stem)
    if not match:
        return None
    return canonical_collection_key(match.group("collection"))


def resolve_collection_asset_skus(
    file_name: str,
    known_skus: Set[str],
    *,
    collection_by_sku: Optional[Dict[str, str]] = None,
    include_rta: bool = False,
) -> List[str]:
    """Fan-out collection vignette/door images to all master SKUs for that style (Plytix packshot behavior)."""
    style = style_code_for_collection_asset(file_name)
    if style:
        prefix = f"{style}-"
        style_matches = sorted(
            sku
            for sku in known_skus
            if sku.upper().startswith(prefix)
            or (
                include_rta
                and sku.upper().startswith(f"RTA-{style}-")
            )
        )
        if style_matches:
            return style_matches

    asset_key = collection_asset_key(file_name)
    if not asset_key or not collection_by_sku:
        return []
    return sorted(
        sku
        for sku in known_skus
        if canonical_collection_key(collection_by_sku.get(sku) or "") == asset_key
    )


def is_styled_image_reference(stem: str) -> bool:
    """True when stem looks like CW-B15 or ASW-BBC42 (render / master image), not CSB36-FP."""
    if "-" not in str(stem or ""):
        return False
    prefix = str(stem).split("-", 1)[0].upper()
    return (
        prefix in MASTER_STYLE_CODES
        or prefix in IMAGE_RENDER_PREFIX_TO_MASTER_STYLES
        or _contextual_render_prefix_styles(prefix, stem) is not None
    )


def extract_diagram_item_stem(file_name: str) -> Optional[str]:
    """Stem before first underscore — B12_Base_... or CSB36-FP_Base_... (not ASW-B15-CLOSED)."""
    name = str(file_name or "").replace("\\", "/").rsplit("/", 1)[-1]
    if "_" not in name:
        return None
    stem = name.split("_", 1)[0].strip()
    if not stem or is_styled_image_reference(stem):
        return None
    return stem.upper()


def resolve_universal_diagram_skus(
    file_name: str,
    known_skus: Set[str],
    *,
    matcher: Optional["MasterSkuImageMatcher"] = None,
) -> List[str]:
    """Attach color-agnostic line drawings to every master SKU for that item (build_plytix load_diagrams)."""
    from db.master_product_images import MasterSkuImageMatcher

    stem = extract_diagram_item_stem(file_name)
    if not stem or not known_skus:
        return []

    exact = sorted(sku for sku in known_skus if split_styled_sku(sku)[1] == stem)
    if exact:
        return exact

    raw_candidates = expand_image_item_master_raw_candidates(stem)
    if raw_candidates:
        catalog_matches = [
            sku
            for sku in known_skus
            if any(
                master_catalog_raw_matches_candidate(split_styled_sku(sku)[1], raw)
                for raw in raw_candidates
            )
        ]
        if catalog_matches:
            return sorted(set(catalog_matches), key=len)

    if matcher is None:
        matcher = MasterSkuImageMatcher.from_known_skus(known_skus)

    image_parsed = parse_image_item_code(stem)
    if not image_parsed:
        return []

    styles = sorted({split_styled_sku(sku)[0] for sku in known_skus if split_styled_sku(sku)[0]})
    matches: List[str] = []
    for style in styles:
        for sku, master in matcher.tribeca_by_style_type.get((style, image_parsed["type_code"]), []):
            if tribeca_image_matches_master(image_parsed, master):
                matches.append(sku)
    return sorted(set(matches), key=len)


def parse_master_raw_sku(raw: str) -> Optional[Dict[str, Any]]:
    """Parse master item code with optional /{high}{variant}/{rest} width range."""
    text = str(raw or "").strip().upper()
    hand_pair = MASTER_HAND_SLASH_RE.match(text)
    if hand_pair:
        width = int(hand_pair.group("width"))
        return {
            "type_code": hand_pair.group(1).upper(),
            "low": width,
            "high": width,
            "variant": hand_pair.group("left").upper(),
            "rest": hand_pair.group("right").upper(),
        }
    slash = MASTER_SLASH_ITEM_RE.match(text)
    if slash:
        return {
            "type_code": slash.group(1).upper(),
            "low": int(slash.group("low")),
            "high": int(slash.group("high")),
            "variant": (slash.group("variant") or "").upper(),
            "rest": (slash.group("rest") or "").upper(),
        }
    type_code, low, remainder = parse_sku(text)
    if not type_code or low is None:
        return None
    remainder = remainder or ""
    range_match = MASTER_RANGE_RE.match(remainder)
    if range_match:
        return {
            "type_code": type_code,
            "low": low,
            "high": int(range_match.group("high")),
            "variant": (range_match.group("variant") or "").upper(),
            "rest": (range_match.group("rest") or "").upper(),
        }
    return {
        "type_code": type_code,
        "low": low,
        "high": low,
        "variant": remainder.upper(),
        "rest": "",
    }


def _parse_gd_w_dimensions(digits: str) -> Tuple[int, int]:
    text = str(digits or "").strip()
    if len(text) == 4:
        return int(text[:2]), int(text[2:])
    if len(text) == 3:
        return int(text[0]), int(text[1:])
    width = int(text[:2])
    height = int(text[2:]) if len(text) > 2 else width
    return width, height


def expand_gd_w_master_raw_candidates(item: str) -> List[str]:
    """Map hyphenated glass-door render codes to master catalog raw SKUs."""
    text = str(item or "").strip().upper()
    ordered: List[str] = []
    seen: Set[str] = set()

    def add(raw: str) -> None:
        value = str(raw or "").strip().upper()
        if value and value not in seen:
            seen.add(value)
            ordered.append(value)

    add(text)
    corner = GD_W_CORNER_ITEM_RE.match(text)
    if corner:
        add(f"GDW{corner.group(1)}{corner.group(2)}")
        return ordered
    glass = GD_W_GLASS_ITEM_RE.match(text)
    if glass:
        add(f"GDW{glass.group(1)}")
        return ordered
    return ordered


def parse_image_item_code(item: str) -> Optional[Dict[str, Any]]:
    """Parse compact image item (BBC42, BEC1124L, FPP9648, BBC45FHL-MC) after style prefix removed."""
    text = str(item or "").strip().upper()
    if not text:
        return None
    corner = GD_W_CORNER_ITEM_RE.match(text)
    if corner:
        width, height = _parse_gd_w_dimensions(corner.group(2))
        return {
            "type_code": f"GDW{corner.group(1).upper()}",
            "width": width,
            "extra_widths": [height],
            "remainder": "",
            "tag": "",
        }
    glass = GD_W_GLASS_ITEM_RE.match(text)
    if glass:
        width, height = _parse_gd_w_dimensions(glass.group(1))
        return {
            "type_code": "GD-W",
            "width": width,
            "extra_widths": [height],
            "remainder": "",
            "tag": "",
        }
    tag = ""
    if "-" in text:
        base, maybe_tag = text.rsplit("-", 1)
        if maybe_tag and maybe_tag.isalnum() and not maybe_tag.isdigit():
            text = base
            tag = maybe_tag

    long_match = IMAGE_LONG_ITEM_RE.match(text)
    if long_match:
        type_code = long_match.group(1).upper()
        digits = long_match.group(2)
        remainder = (long_match.group(3) or "").upper()
        if len(digits) == 4 and not remainder:
            return {
                "type_code": type_code,
                "width": int(digits[:2]),
                "extra_widths": [int(digits[2:])],
                "remainder": remainder,
                "tag": tag.upper(),
            }
        if len(digits) == 4 and remainder:
            if type_code == "VSD" and remainder in {"DL", "DR", "DLR", "LR", "RL"}:
                return {
                    "type_code": type_code,
                    "width": int(digits[:2]),
                    "extra_widths": [int(digits[2:])],
                    "remainder": remainder,
                    "tag": tag.upper(),
                }
            if len(remainder) == 1:
                return {
                    "type_code": type_code,
                    "width": int(digits),
                    "extra_widths": [],
                    "remainder": remainder,
                    "tag": tag.upper(),
                }
            return {
                "type_code": type_code,
                "width": int(digits[:2]),
                "extra_widths": [int(digits[2:])],
                "remainder": "",
                "tag": tag.upper(),
            }
        return {
            "type_code": type_code,
            "width": int(digits),
            "extra_widths": [],
            "remainder": remainder,
            "tag": tag.upper(),
        }

    type_code, width, remainder = parse_sku(text)
    if not type_code or width is None:
        return None
    return {
        "type_code": type_code,
        "width": width,
        "extra_widths": [],
        "remainder": (remainder or "").upper(),
        "tag": tag.upper(),
    }


def _image_widths(image_parsed: Dict[str, Any]) -> List[int]:
    widths = [int(image_parsed["width"])]
    for extra in image_parsed.get("extra_widths") or []:
        widths.append(int(extra))
    return widths


def tribeca_image_matches_master(image_parsed: Dict[str, Any], master_parsed: Dict[str, Any]) -> bool:
    if image_parsed["type_code"] != master_parsed["type_code"]:
        return False
    master_widths = {master_parsed["low"], master_parsed["high"]}
    if not any(width in master_widths for width in _image_widths(image_parsed)):
        return False

    image_variant = image_parsed.get("remainder") or ""
    master_variant = master_parsed.get("variant") or ""
    image_tag = image_parsed.get("tag") or ""
    master_rest = master_parsed.get("rest") or ""

    if image_variant:
        if image_variant == master_variant:
            pass
        elif len(master_rest) == 1 and image_variant == master_rest:
            pass
        elif image_variant in ("LR", "RL"):
            pass
        else:
            return False
    elif len(master_variant) > 1:
        return False

    if image_tag:
        return master_rest == image_tag or master_rest.endswith(f"-{image_tag}")

    if master_rest:
        if len(master_rest) == 1 and master_rest.isalpha():
            return True
        return "-" not in master_rest

    return True


def _normalize_catalog_lookup_token(raw: str) -> str:
    """Normalize diagram / lookup tokens (e.g. FPP9648 1-4 -> FPP9648 1/4)."""
    text = str(raw or "").strip().upper()
    return re.sub(r"(\d)-(\d)\b", r"\1/\2", text)


def _expand_vsd_master_raw_candidates(widths: Sequence[int], hand: str) -> List[str]:
    """Map Tribeca vanity render codes to master {prefix}-VSD{width}21{hand} rows."""
    out: List[str] = []
    for width in sorted({int(value) for value in widths}):
        if width >= 48:
            out.append(f"VSD{width}21DLR")
        elif hand == "DR":
            out.append(f"VSD{width}21DR")
        elif hand == "DL":
            out.append(f"VSD{width}21DL")
        else:
            out.append(f"VSD{width}21DL")
            out.append(f"VSD{width}21DR")
    return out


def master_catalog_raw_matches_candidate(master_raw: str, candidate_raw: str) -> bool:
    """True when master raw equals or extends a catalog lookup token (e.g. FPP9648 -> FPP9648 1/4)."""
    master = _normalize_catalog_lookup_token(master_raw)
    candidate = _normalize_catalog_lookup_token(candidate_raw)
    if not master or not candidate:
        return False
    if master == candidate:
        return True
    if master.startswith(f"{candidate} ") or master.startswith(f"{candidate}-"):
        return True
    return False


def expand_image_item_master_raw_candidates(item: str) -> List[str]:
    """Map Tribeca render / diagram item codes to imported master catalog raw SKUs."""
    text = str(item or "").strip().upper()
    ordered: List[str] = []
    seen: Set[str] = set()

    def add(raw: str) -> None:
        value = _normalize_catalog_lookup_token(raw)
        if value and value not in seen:
            seen.add(value)
            ordered.append(value)

    add(text)
    rollout = ROLLOUT_TRAY_ITEM_RE.match(text)
    if rollout:
        add(f"{int(rollout.group(1))}BT")
    oven = OVEN_DOUBLE_DIAGRAM_RE.match(text)
    if oven:
        add(f"OVD{oven.group(1)}{oven.group(2)}")
    for raw in expand_gd_w_master_raw_candidates(text):
        add(raw)
    parsed = parse_image_item_code(text)
    if not parsed:
        return ordered

    type_code = parsed["type_code"]
    hand = (parsed.get("remainder") or "").upper()
    tag = (parsed.get("tag") or "").upper()
    width = int(parsed["width"])
    extra_widths = [int(value) for value in parsed.get("extra_widths") or []]

    if type_code == "ROTD" and width:
        add(f"{width}BT")
    if type_code == "M" and extra_widths:
        add(f"M{width}{extra_widths[0]}")
    if type_code == "BEC" and width == 1124 and hand in ("L", "R"):
        add(f"BEC1224{hand}")
    if type_code == "DWR" and width == 3 and hand in ("L", ""):
        add("DWR3L/R")
    if type_code == "FPP" and width == 96 and 48 in extra_widths:
        add("FPP9648")
        add("FPPB9648")
    if type_code == "BCOR":
        if hand in ("LR", "RL", "L", "R"):
            add(f"BCOR{width}L/R")
    if type_code == "CSB":
        if tag == "FP":
            add(f"CSB{width}-FP")
        elif not tag:
            add(f"CSB{width}")
    if type_code == "VSD":
        for raw in _expand_vsd_master_raw_candidates([width, *extra_widths], hand):
            add(raw)

    return ordered


def match_master_skus_for_catalog_image_item(
    image_key: str,
    known_skus: Set[str],
    *,
    allow_render_prefix: bool = True,
    context: Optional[str] = None,
) -> List[str]:
    """Resolve Tribeca image keys to compact master catalog SKUs (BEC1224L, DWR3L/R, …)."""
    style, item = split_styled_sku(image_key)
    if not style or not item:
        return []
    raw_candidates = expand_image_item_master_raw_candidates(item)
    parsed = parse_image_item_code(item)
    cross_style = bool(parsed and parsed.get("type_code") == "VSD")
    master_styles = (
        master_style_codes_for_image_prefix(style, context=context or image_key)
        if allow_render_prefix
        else [style.upper()]
    )
    search_styles = (
        sorted({split_styled_sku(sku)[0] for sku in known_skus if split_styled_sku(sku)[0]})
        if cross_style
        else master_styles
    )
    matches: List[str] = []
    for master_style in search_styles:
        prefix = f"{master_style}-"
        for sku in known_skus:
            if not sku.upper().startswith(prefix):
                continue
            master_raw = split_styled_sku(sku)[1]
            if any(master_catalog_raw_matches_candidate(master_raw, raw) for raw in raw_candidates):
                matches.append(sku)
    return sorted(set(matches), key=len)


def master_sku_matches_tribeca_image_key(full_master_sku: str, image_key: str) -> bool:
    """Match styled image key to master using Tribeca parse + render-prefix normalization."""
    image_style, image_item = split_styled_sku(image_key)
    master_style, master_raw = split_styled_sku(full_master_sku)
    if not image_style:
        return False
    if master_style not in master_style_codes_for_image_prefix(image_style, context=image_key):
        return False
    image_parsed = parse_image_item_code(image_item)
    master_parsed = parse_master_raw_sku(master_raw)
    if not image_parsed or not master_parsed:
        return False
    return tribeca_image_matches_master(image_parsed, master_parsed)

def image_filename_item_sku(stem: str) -> Tuple[str, str]:
    """build_plytix_csv load_image_index: parts[0]=prefix/style, parts[1]=item SKU."""
    parts = str(stem or "").split("-")
    if len(parts) < 2:
        return "", stem.upper()
    return parts[0].upper(), parts[1].upper()
