<?php
/**
 * Shared admin configuration helpers (draft orders, HubSpot, detail page).
 */
if (!function_exists('humanize_token')) {
    function humanize_token($v) {
        $s = trim((string)$v);
        if ($s === '') return '';
        $s = str_replace(['_', '-'], ' ', $s);
        $s = preg_replace('/\s+/', ' ', $s);
        return ucwords(strtolower((string)$s));
    }
}

if (!function_exists('admin_hardware_selection_row_is_casing')) {
    /** True when a structured row belongs in Casing, not Hardware. */
    function admin_hardware_selection_row_is_casing(array $row) {
        $title = trim((string) ($row['title'] ?? ''));
        $sku = trim((string) ($row['sku'] ?? ''));
        $blob = trim($title . ' ' . $sku);
        if ($blob !== '') {
            if (preg_match('/\bcasing\b/i', $blob)) {
                return true;
            }
            if (preg_match('/\b(?:IC\d+|EC\d+|CT\d+)\b/i', $blob)) {
                return true;
            }
        }
        $hasLength = trim((string) ($row['length_label'] ?? $row['length_value'] ?? '')) !== '';
        $hasFinish = trim((string) ($row['finish_label'] ?? $row['finish_value'] ?? '')) !== '';
        $hasThick = trim((string) ($row['thickness_label'] ?? $row['thickness_value'] ?? '')) !== '';
        $fn = trim((string) ($row['function_label'] ?? $row['function_value'] ?? ''));
        if ($hasLength && !$hasFinish && !$hasThick) {
            return true;
        }
        if ($fn !== '' && !$hasFinish && !$hasThick && preg_match("/^\d+(?:\s*\/\s*\d+)?\s*'?\s*$/u", $fn)) {
            return true;
        }
        return false;
    }
}

if (!function_exists('admin_filter_hardware_selections')) {
    function admin_filter_hardware_selections(array $items) {
        $out = [];
        foreach ($items as $row) {
            if (!is_array($row)) {
                continue;
            }
            if (admin_hardware_selection_row_is_casing($row)) {
                continue;
            }
            $out[] = $row;
        }
        return $out;
    }
}

if (!function_exists('admin_strip_casing_from_hardware_options')) {
    function admin_strip_casing_from_hardware_options(array $options) {
        foreach (['_hardware_selections', 'hardware_options'] as $key) {
            if (!isset($options[$key]) || !is_array($options[$key])) {
                continue;
            }
            $normalized = admin_normalize_hardware_selections($options[$key]);
            $filtered = admin_filter_hardware_selections($normalized);
            if (empty($filtered)) {
                unset($options[$key]);
            } else {
                $options[$key] = $filtered;
            }
        }
        return $options;
    }
}

if (!function_exists('admin_get_hardware_selections')) {
    function admin_get_hardware_selections(array $options) {
        $hw = $options['_hardware_selections'] ?? $options['hardware_options'] ?? null;
        if (!is_array($hw)) {
            return [];
        }
        if (isset($hw['product_id']) || isset($hw['title'])) {
            $hw = [$hw];
        }
        $out = [];
        foreach ($hw as $item) {
            if (!is_array($item)) {
                continue;
            }
            if (empty($item['product_id']) && empty($item['title'])) {
                continue;
            }
            $out[] = $item;
        }
        return $out;
    }
}

if (!function_exists('admin_parse_hardware_summary_text')) {
    function admin_parse_hardware_summary_text($value) {
        $value = trim((string) $value);
        if ($value === '') {
            return [];
        }
        $row = ['title' => $value, 'qty' => 1];
        if (preg_match('/\(×(\d+)\)\s*$/u', $value, $m)) {
            $row['qty'] = max(1, (int) $m[1]);
            $value = trim((string) preg_replace('/\(×\d+\)\s*$/u', '', $value));
        }
        $parts = array_values(array_filter(array_map('trim', preg_split('/\s+—\s+/u', $value)), function ($p) {
            return $p !== '';
        }));
        if (count($parts) >= 3) {
            $row['function_label'] = array_pop($parts);
            $row['finish_label'] = array_pop($parts);
            $row['title'] = implode(' — ', $parts);
        } elseif (count($parts) === 2) {
            $row['finish_label'] = $parts[1];
            $row['title'] = $parts[0];
        } else {
            $row['title'] = $value;
        }
        return $row;
    }
}

if (!function_exists('admin_hardware_selections_from_selected_summary')) {
    function admin_hardware_selections_from_selected_summary(array $options) {
        $summary = $options['_selected_summary'] ?? null;
        if (!is_array($summary)) {
            return [];
        }
        $out = [];
        foreach ($summary as $item) {
            if (!is_array($item)) {
                continue;
            }
            $label = trim((string) ($item['label'] ?? ''));
            if ($label === '' || !preg_match('/^hardware(\s+\d+)?$/i', $label)) {
                continue;
            }
            $parsed = admin_parse_hardware_summary_text($item['value'] ?? '');
            if (empty($parsed)) {
                continue;
            }
            if (admin_hardware_selection_row_is_casing($parsed)) {
                continue;
            }
            $img = trim((string) ($item['image'] ?? ''));
            if ($img !== '') {
                $parsed['image'] = $img;
            }
            $out[] = $parsed;
        }
        return $out;
    }
}

if (!function_exists('admin_deduplicate_hardware_selections')) {
    function admin_deduplicate_hardware_selections(array $items) {
        $out = [];
        foreach ($items as $row) {
            if (!is_array($row)) {
                continue;
            }
            $key = strtolower(trim((string) ($row['product_id'] ?? '')));
            if ($key === '') {
                $key = strtolower(trim((string) ($row['title'] ?? '')));
            }
            if ($key === '') {
                $out[] = $row;
                continue;
            }
            if (!isset($out[$key])) {
                $out[$key] = $row;
                continue;
            }
            foreach ($row as $k => $v) {
                if ($v === null || $v === '') {
                    continue;
                }
                if (empty($out[$key][$k])) {
                    $out[$key][$k] = $v;
                }
            }
        }
        return array_values($out);
    }
}

if (!function_exists('admin_resolve_hardware_selections')) {
    function admin_resolve_hardware_selections(array $options) {
        $structured = admin_get_hardware_selections($options);
        if (!empty($structured)) {
            return admin_deduplicate_hardware_selections(admin_filter_hardware_selections($structured));
        }
        return admin_deduplicate_hardware_selections(
            admin_filter_hardware_selections(admin_hardware_selections_from_selected_summary($options))
        );
    }
}

if (!function_exists('admin_hardware_summary_parts')) {
    function admin_hardware_summary_parts(array $options) {
        $parts = [];
        $n = 0;
        foreach (admin_resolve_hardware_selections($options) as $item) {
            $title = trim((string)($item['title'] ?? $item['product_id'] ?? ''));
            if ($title === '') continue;
            $line = $title;
            $fin = trim((string)($item['finish_label'] ?? ''));
            if ($fin !== '') $line .= ' — ' . $fin;
            $fn = trim((string)($item['function_label'] ?? ''));
            if ($fn !== '') $line .= ' — ' . $fn;
            $thick = trim((string)($item['thickness_label'] ?? ''));
            if ($thick === '' && !empty($item['thickness_value'])) {
                $thick = humanize_token($item['thickness_value']);
            }
            if ($thick !== '') $line .= ' — ' . $thick;
            $q = (int)($item['qty'] ?? 1);
            if ($q > 1) $line .= ' (×' . $q . ')';
            $parts[] = ($n === 0 ? 'Hardware' : 'Hardware ' . ($n + 1)) . ': ' . $line;
            $n++;
        }
        return $parts;
    }
}

if (!function_exists('admin_normalize_hardware_selections')) {
    function admin_normalize_hardware_selections($raw) {
        if (!is_array($raw)) {
            return [];
        }
        if (isset($raw['product_id']) || isset($raw['title'])) {
            $raw = [$raw];
        }
        $out = [];
        foreach ($raw as $item) {
            if (!is_array($item)) {
                continue;
            }
            if (empty($item['product_id']) && empty($item['title'])) {
                continue;
            }
            $out[] = $item;
        }
        return $out;
    }
}

if (!function_exists('admin_hardware_selection_row_key')) {
    function admin_hardware_selection_row_key(array $row, $index = 0) {
        $pid = trim((string) ($row['product_id'] ?? ''));
        if ($pid !== '') {
            return 'pid:' . strtolower($pid);
        }
        $title = strtolower(trim((string) ($row['title'] ?? '')));
        if ($title !== '') {
            return 'title:' . $title;
        }
        return 'idx:' . (int) $index;
    }
}

if (!function_exists('admin_merge_hardware_selection_rows')) {
    function admin_merge_hardware_selection_rows(array $base, array $overlay) {
        $out = is_array($base) ? $base : [];
        foreach ($overlay as $k => $v) {
            if ($v === null || $v === '') {
                continue;
            }
            $out[$k] = $v;
        }
        return $out;
    }
}

if (!function_exists('admin_merge_hardware_admin_post')) {
    /** Preserve image/title/price from saved rows when admin edits finish/function/thickness/qty. */
    function admin_merge_hardware_admin_post(array $existing, array $posted) {
        $existing = admin_normalize_hardware_selections($existing);
        $posted = admin_normalize_hardware_selections($posted);
        $posted = admin_filter_hardware_selections($posted);
        if (empty($posted)) {
            return [];
        }
        if (empty($existing)) {
            return $posted;
        }
        $byId = [];
        foreach ($existing as $idx => $row) {
            if (!is_array($row)) {
                continue;
            }
            $byId[admin_hardware_selection_row_key($row, $idx)] = $row;
        }
        $out = [];
        foreach ($posted as $idx => $row) {
            if (!is_array($row)) {
                continue;
            }
            $base = $byId[admin_hardware_selection_row_key($row, $idx)] ?? ($existing[$idx] ?? []);
            $merged = admin_merge_hardware_selection_rows(is_array($base) ? $base : [], $row);
            $out[] = admin_strip_blank_hardware_row_fields($merged);
        }
        return admin_filter_hardware_selections(admin_normalize_hardware_selections($out));
    }
}

if (!function_exists('admin_hardware_field_choices_for_item')) {
    /**
     * Build dropdown choices for one hardware field on a saved line item.
     *
     * @return array<int, array{value: string, label: string}>
     */
    function admin_hardware_field_choices_for_item(array $item, $optionsKey, $valueKey, $labelKey) {
        $choices = [];
        if (!empty($item[$optionsKey]) && is_array($item[$optionsKey])) {
            foreach ($item[$optionsKey] as $opt) {
                if (!is_array($opt)) {
                    $s = trim((string) $opt);
                    if ($s === '') {
                        continue;
                    }
                    $choices[] = ['value' => $s, 'label' => humanize_token($s)];
                    continue;
                }
                $v = trim((string) ($opt['value'] ?? $opt['label'] ?? ''));
                $l = trim((string) ($opt['label'] ?? $opt['value'] ?? ''));
                if ($v === '' && $l === '') {
                    continue;
                }
                $choices[] = [
                    'value' => $v !== '' ? $v : $l,
                    'label' => $l !== '' ? $l : humanize_token($v),
                ];
            }
        }
        $curVal = trim((string) ($item[$valueKey] ?? ''));
        $curLbl = trim((string) ($item[$labelKey] ?? $curVal));
        if ($curLbl === '' && $curVal !== '') {
            $curLbl = humanize_token($curVal);
        }
        if ($curVal !== '' || $curLbl !== '') {
            $found = false;
            foreach ($choices as $c) {
                if (($c['value'] ?? '') === $curVal || ($c['label'] ?? '') === $curLbl) {
                    $found = true;
                    break;
                }
            }
            if (!$found) {
                array_unshift($choices, [
                    'value' => $curVal !== '' ? $curVal : $curLbl,
                    'label' => $curLbl !== '' ? $curLbl : humanize_token($curVal),
                ]);
            }
        }
        return $choices;
    }
}

if (!function_exists('admin_hardware_finish_choices_for_item')) {
    function admin_hardware_finish_choices_for_item(array $item, array $options = []) {
        foreach (admin_hardware_landing_field_defs() as $def) {
            if (!is_array($def) || (string) ($def['value_key'] ?? '') !== 'finish_value') {
                continue;
            }
            return admin_hardware_merged_field_choices_for_item($item, $options, $def);
        }
        return admin_hardware_field_choices_for_item($item, 'finish_options', 'finish_value', 'finish_label');
    }
}

if (!function_exists('admin_hardware_function_choices_for_item')) {
    function admin_hardware_function_choices_for_item(array $item, array $options = []) {
        foreach (admin_hardware_landing_field_defs() as $def) {
            if (!is_array($def) || (string) ($def['value_key'] ?? '') !== 'function_value') {
                continue;
            }
            return admin_hardware_merged_field_choices_for_item($item, $options, $def);
        }
        return admin_hardware_field_choices_for_item($item, 'function_options', 'function_value', 'function_label');
    }
}

if (!function_exists('admin_hardware_thickness_choices_for_item')) {
    function admin_hardware_thickness_choices_for_item(array $item, array $options = []) {
        foreach (admin_hardware_landing_field_defs() as $def) {
            if (!is_array($def) || (string) ($def['value_key'] ?? '') !== 'thickness_value') {
                continue;
            }
            return admin_hardware_merged_field_choices_for_item($item, $options, $def);
        }
        return admin_hardware_field_choices_for_item($item, 'thickness_options', 'thickness_value', 'thickness_label');
    }
}

if (!function_exists('admin_hardware_field_schema_needles')) {
    /**
     * All lookup needles for one hardware field (metaobject key, label, legacy aliases).
     *
     * @return list<string>
     */
    function admin_hardware_field_schema_needles(array $def) {
        $needles = [];
        foreach (array_merge(
            [(string) ($def['schema_id'] ?? '')],
            (array) ($def['schema_id_aliases'] ?? []),
            (array) ($def['summary_labels'] ?? []),
            [(string) ($def['prop_label'] ?? '')]
        ) as $needle) {
            $needle = trim((string) $needle);
            if ($needle !== '') {
                $needles[] = $needle;
            }
        }
        return array_values(array_unique($needles));
    }
}

if (!function_exists('admin_register_storefront_choice_map_entries')) {
    /** Register the same choice list under every normalized needle key. */
    function admin_register_storefront_choice_map_entries(array &$map, array $needles, array $choices) {
        if (empty($choices)) {
            return;
        }
        foreach ($needles as $needle) {
            $nl = admin_norm_selected_option_label($needle);
            if ($nl === '') {
                continue;
            }
            $map[$nl] = $choices;
        }
    }
}

if (!function_exists('admin_find_hardware_field_def_for_metaobject_key')) {
    /** Match a door_config_options metaobject key/label to a hardware field def. */
    function admin_find_hardware_field_def_for_metaobject_key($optionKey, $optionLabel = '') {
        $candidates = array_values(array_filter([
            trim((string) $optionKey),
            trim((string) $optionLabel),
        ]));
        if (empty($candidates)) {
            return null;
        }
        foreach (admin_hardware_landing_field_defs() as $def) {
            if (!is_array($def)) {
                continue;
            }
            foreach (admin_hardware_field_schema_needles($def) as $needle) {
                $nn = admin_norm_selected_option_label($needle);
                foreach ($candidates as $candidate) {
                    if ($nn !== '' && $nn === admin_norm_selected_option_label($candidate)) {
                        return $def;
                    }
                }
            }
        }
        return null;
    }
}

if (!function_exists('admin_parse_product_list_metafield_choices')) {
    /**
     * Parse a Shopify list / comma metafield into dropdown choices.
     *
     * @return list<array{value: string, label: string}>
     */
    function admin_parse_product_list_metafield_choices($rawValue, $rawType = '') {
        $items = [];
        $raw = trim((string) $rawValue);
        if ($raw === '') {
            return [];
        }
        $decoded = json_decode($raw, true);
        if (is_array($decoded)) {
            $items = $decoded;
        } else {
            $items = preg_split('/\s*,\s*/', $raw);
        }
        $choices = [];
        foreach ($items as $item) {
            if (is_array($item)) {
                $v = trim((string) ($item['value'] ?? $item['label'] ?? ''));
                $l = trim((string) ($item['label'] ?? $item['value'] ?? ''));
            } else {
                $v = trim((string) $item);
                $l = $v;
            }
            if ($v === '' && $l === '') {
                continue;
            }
            if ($v === '') {
                $v = $l;
            }
            if ($l === '') {
                $l = function_exists('humanize_token') ? humanize_token($v) : $v;
            }
            $choices[] = ['value' => $v, 'label' => $l];
        }
        return admin_normalize_choice_list($choices);
    }
}

if (!function_exists('admin_append_hardware_product_list_metafield_choices')) {
    /** Add thickness/backset list metafields (not always in door_config_options) to the live map. */
    function admin_append_hardware_product_list_metafield_choices(array &$map, $productId) {
        if ($productId === '' || !function_exists('admin_shopify_is_configured') || !admin_shopify_is_configured()) {
            return;
        }
        $prodQuery = <<<'GQL'
query($id: ID!) {
  product(id: $id) {
    thickness: metafield(namespace: "custom", key: "hardware_thickness_option") { value type }
    backset: metafield(namespace: "custom", key: "Door_Backset") { value type }
    backsetAlt: metafield(namespace: "custom", key: "door_backset") { value type }
  }
}
GQL;
        $pres = admin_shopify_gql_request($prodQuery, ['id' => 'gid://shopify/Product/' . $productId]);
        if (empty($pres['ok'])) {
            return;
        }
        $product = $pres['data']['product'] ?? [];
        $pairs = [
            ['def' => admin_find_hardware_field_def_for_metaobject_key('door_thickness', 'Door Thickness'), 'mf' => $product['thickness'] ?? null],
            ['def' => admin_find_hardware_field_def_for_metaobject_key('door_backset', 'Door Backset'), 'mf' => $product['backset'] ?? ($product['backsetAlt'] ?? null)],
        ];
        foreach ($pairs as $pair) {
            $def = $pair['def'];
            $mf = $pair['mf'];
            if (!is_array($def) || !is_array($mf)) {
                continue;
            }
            $choices = admin_parse_product_list_metafield_choices($mf['value'] ?? '', $mf['type'] ?? '');
            if (empty($choices)) {
                continue;
            }
            admin_register_storefront_choice_map_entries($map, admin_hardware_field_schema_needles($def), $choices);
        }
    }
}

if (!function_exists('admin_strip_blank_hardware_row_fields')) {
    /** Remove empty hardware value/label pairs so admin does not persist blank selections. */
    function admin_strip_blank_hardware_row_fields(array $row) {
        foreach (admin_hardware_landing_field_defs() as $def) {
            if (!is_array($def)) {
                continue;
            }
            $vk = (string) ($def['value_key'] ?? '');
            $lk = (string) ($def['label_key'] ?? '');
            if ($vk === '' && $lk === '') {
                continue;
            }
            $val = $vk !== '' ? trim((string) ($row[$vk] ?? '')) : '';
            $lbl = $lk !== '' ? trim((string) ($row[$lk] ?? '')) : '';
            if ($val === '' && $lbl === '') {
                if ($vk !== '') {
                    unset($row[$vk]);
                }
                if ($lk !== '') {
                    unset($row[$lk]);
                }
                $ok = (string) ($def['options_key'] ?? '');
                if ($ok !== '') {
                    unset($row[$ok]);
                }
            }
        }
        return $row;
    }
}

if (!function_exists('admin_hardware_landing_field_defs')) {
    /**
     * Landing-hardware line-item fields mirrored on the storefront and admin edit page.
     * Each row maps summary / line-item label(s) to persisted _hardware_selections keys.
     * schema_id mirrors custom.door_config_options metaobject keys on the hardware product.
     *
     * @return list<array{summary_labels: list<string>, prop_label: string, schema_id: string, schema_id_aliases: list<string>, options_key: string, value_key: string, label_key: string}>
     */
    function admin_hardware_landing_field_defs() {
        return [
            [
                'summary_labels'     => ['finish'],
                'prop_label'         => 'Finish',
                'schema_id'          => 'finish',
                'schema_id_aliases'  => [],
                'allow_metafields'   => ['hardware_finish'],
                'options_key'        => 'finish_options',
                'value_key'          => 'finish_value',
                'label_key'          => 'finish_label',
            ],
            [
                'summary_labels'     => ['hardware function', 'function'],
                'prop_label'         => 'Hardware Function',
                'schema_id'          => 'function',
                'schema_id_aliases'  => ['hardware_function'],
                'allow_metafields'   => ['hardware_function'],
                'options_key'        => 'function_options',
                'value_key'          => 'function_value',
                'label_key'          => 'function_label',
            ],
            [
                'summary_labels'     => ['closer color'],
                'prop_label'         => 'Closer Color',
                'schema_id'          => 'closer_color',
                'schema_id_aliases'  => ['closer-color', 'closer color'],
                'allow_metafields'   => ['closer_color'],
                'options_key'        => 'closer_color_options',
                'value_key'          => 'closer_color_value',
                'label_key'          => 'closer_color_label',
            ],
            [
                'summary_labels'     => ['door thickness', 'thickness'],
                'prop_label'         => 'Door Thickness',
                'schema_id'          => 'door_thickness',
                'schema_id_aliases'  => ['thickness', 'hardware_thickness_option'],
                'allow_metafields'   => [],
                'options_key'        => 'thickness_options',
                'value_key'          => 'thickness_value',
                'label_key'          => 'thickness_label',
            ],
            [
                'summary_labels'     => ['swing direction'],
                'prop_label'         => 'Swing Direction',
                'schema_id'          => 'swing_direction',
                'schema_id_aliases'  => ['swing-direction', 'swing direction'],
                'allow_metafields'   => ['swing_direction'],
                'options_key'        => 'swing_direction_options',
                'value_key'          => 'swing_direction_value',
                'label_key'          => 'swing_direction_label',
            ],
            [
                'summary_labels'     => ['door backset', 'backset'],
                'prop_label'         => 'Door Backset',
                'schema_id'          => 'door_backset',
                'schema_id_aliases'  => ['backset', 'Door_Backset', 'door_backset'],
                'allow_metafields'   => [],
                'options_key'        => 'backset_options',
                'value_key'          => 'backset_value',
                'label_key'          => 'backset_label',
            ],
            [
                'summary_labels'     => ['track length'],
                'prop_label'         => 'Track Length',
                'schema_id'          => 'track_length',
                'schema_id_aliases'  => ['door_hanger_tracker_length', 'door-hanger-tracker-length', 'door hanger tracker length', 'track-length', 'track length'],
                'allow_metafields'   => ['door_hanger_tracker_length', 'track_length'],
                'options_key'        => 'track_length_options',
                'value_key'          => 'track_length_value',
                'label_key'          => 'track_length_label',
            ],
            [
                'summary_labels'     => ['number of connected doors', 'connected doors'],
                'prop_label'         => 'Number of Connected Doors',
                'schema_id'          => 'connected_doors',
                'schema_id_aliases'  => ['number_of_connected_doors', 'number-of-connected-doors', 'number of connected doors', 'connected-doors'],
                'allow_metafields'   => ['number_of_connected_doors', 'connected_doors'],
                'options_key'        => 'connected_doors_options',
                'value_key'          => 'connected_doors_value',
                'label_key'          => 'connected_doors_label',
            ],
            [
                'summary_labels'     => ['upper track length'],
                'prop_label'         => 'Upper Track Length',
                'schema_id'          => 'upper_track_length',
                'schema_id_aliases'  => ['upper-track-length', 'upper track length'],
                'allow_metafields'   => ['upper_track_length'],
                'options_key'        => 'upper_track_length_options',
                'value_key'          => 'upper_track_length_value',
                'label_key'          => 'upper_track_length_label',
            ],
            [
                'summary_labels'     => ['max door size'],
                'prop_label'         => 'Max Door Size',
                'schema_id'          => 'max_door_size',
                'schema_id_aliases'  => ['max-door-size', 'max door size'],
                'allow_metafields'   => ['max_door_size'],
                'options_key'        => 'max_door_size_options',
                'value_key'          => 'max_door_size_value',
                'label_key'          => 'max_door_size_label',
            ],
            [
                'summary_labels'     => ['carriage pack type'],
                'prop_label'         => 'Carriage Pack Type',
                'schema_id'          => 'carriage_pack_type',
                'schema_id_aliases'  => ['carriage-pack-type', 'carriage pack type'],
                'allow_metafields'   => ['carriage_pack_type'],
                'options_key'        => 'carriage_pack_type_options',
                'value_key'          => 'carriage_pack_type_value',
                'label_key'          => 'carriage_pack_type_label',
            ],
            [
                'summary_labels'     => ['strap hinge length'],
                'prop_label'         => 'Strap Hinge Length',
                'schema_id'          => 'strap_hinge_length',
                'schema_id_aliases'  => ['strap-hinge-length', 'strap hinge length'],
                'allow_metafields'   => ['strap_hinge_length'],
                'options_key'        => 'strap_hinge_length_options',
                'value_key'          => 'strap_hinge_length_value',
                'label_key'          => 'strap_hinge_length_label',
            ],
        ];
    }
}

if (!function_exists('admin_hardware_extra_field_choices_for_item')) {
    function admin_hardware_extra_field_choices_for_item(array $item, array $def, array $options = []) {
        if (!is_array($def)) {
            return [];
        }
        return admin_hardware_merged_field_choices_for_item($item, $options, $def);
    }
}

if (!function_exists('admin_is_hardware_landing_schema_id')) {
    function admin_is_hardware_landing_schema_id($id) {
        $norm = admin_norm_option_id((string) $id);
        if ($norm === '') {
            return false;
        }
        static $ids = null;
        if ($ids === null) {
            $ids = [];
            foreach (admin_hardware_landing_field_defs() as $def) {
                if (!is_array($def)) {
                    continue;
                }
                foreach (admin_hardware_field_schema_needles($def) as $needle) {
                    $sid = admin_norm_option_id($needle);
                    if ($sid !== '') {
                        $ids[$sid] = true;
                    }
                }
            }
        }
        return !empty($ids[$norm]);
    }
}

if (!function_exists('admin_is_hardware_landing_field_label')) {
    function admin_is_hardware_landing_field_label($label) {
        $nl = admin_norm_selected_option_label($label);
        if ($nl === '') {
            return false;
        }
        foreach (admin_hardware_landing_field_defs() as $def) {
            if (!is_array($def)) {
                continue;
            }
            $prop = admin_norm_selected_option_label((string) ($def['prop_label'] ?? ''));
            if ($prop !== '' && $prop === $nl) {
                return true;
            }
            foreach ((array) ($def['summary_labels'] ?? []) as $candidate) {
                if ($nl === admin_norm_selected_option_label($candidate)) {
                    return true;
                }
            }
        }
        return false;
    }
}

if (!function_exists('admin_pick_saved_landing_hardware_field_pair')) {
    /**
     * Authoritative saved value for one landing-hardware field.
     * Priority: _hardware_selections row → _line_item_properties → _selected_summary → schema keys.
     *
     * @return array{value: string, label: string}
     */
    function admin_pick_saved_landing_hardware_field_pair(array $options, array $def) {
        $propLabel = trim((string) ($def['prop_label'] ?? ''));
        $labelKey = trim((string) ($def['label_key'] ?? ''));
        $valueKey = trim((string) ($def['value_key'] ?? ''));
        $schemaId = trim((string) ($def['schema_id'] ?? ''));
        $summaryLabels = array_values(array_filter(array_map('strval', (array) ($def['summary_labels'] ?? []))));

        foreach (admin_resolve_hardware_selections($options) as $row) {
            if (!is_array($row)) {
                continue;
            }
            $lbl = $labelKey !== '' ? trim((string) ($row[$labelKey] ?? '')) : '';
            $val = $valueKey !== '' ? trim((string) ($row[$valueKey] ?? '')) : '';
            if ($lbl !== '' || $val !== '') {
                return [
                    'value' => $val !== '' ? $val : $lbl,
                    'label' => $lbl !== '' ? $lbl : $val,
                ];
            }
        }

        if ($propLabel !== '' && isset($options['_line_item_properties']) && is_array($options['_line_item_properties'])) {
            foreach ($options['_line_item_properties'] as $k => $v) {
                if (admin_norm_selected_option_label((string) $k) === admin_norm_selected_option_label($propLabel)) {
                    $s = trim((string) $v);
                    if ($s !== '') {
                        return ['value' => $s, 'label' => $s];
                    }
                }
            }
        }

        if (isset($options['_selected_summary']) && is_array($options['_selected_summary'])) {
            foreach ($options['_selected_summary'] as $item) {
                if (!is_array($item)) {
                    continue;
                }
                $lab = admin_norm_selected_option_label((string) ($item['label'] ?? ''));
                $match = ($propLabel !== '' && $lab === admin_norm_selected_option_label($propLabel));
                if (!$match) {
                    foreach ($summaryLabels as $candidate) {
                        if ($lab === admin_norm_selected_option_label($candidate)) {
                            $match = true;
                            break;
                        }
                    }
                }
                if (!$match) {
                    continue;
                }
                $s = trim((string) ($item['value'] ?? ''));
                if ($s !== '') {
                    return ['value' => $s, 'label' => $s];
                }
            }
        }

        if ($schemaId !== '' && function_exists('admin_config_get_value')) {
            $schemaRaw = admin_config_get_value($options, $schemaId);
            if (is_array($schemaRaw)) {
                $schemaRaw = implode(', ', array_map('strval', $schemaRaw));
            }
            $schemaRaw = trim((string) ($schemaRaw === null ? '' : $schemaRaw));
            if ($schemaRaw !== '') {
                return ['value' => $schemaRaw, 'label' => $schemaRaw];
            }
        }

        return ['value' => '', 'label' => ''];
    }
}

if (!function_exists('admin_pick_saved_landing_hardware_display')) {
    /** Read a saved landing-hardware display string from summary, line-item props, or hardware row. */
    function admin_pick_saved_landing_hardware_display(array $options, array $summaryLabels, array $def = []) {
        if (!empty($def)) {
            $pair = admin_pick_saved_landing_hardware_field_pair($options, $def);
            return (string) ($pair['label'] !== '' ? $pair['label'] : $pair['value']);
        }
        $summaryLabels = array_values(array_filter(array_map('strval', $summaryLabels)));
        $propLabel = trim((string) ($def['prop_label'] ?? ''));
        $labelKey = trim((string) ($def['label_key'] ?? ''));
        $valueKey = trim((string) ($def['value_key'] ?? ''));
        $schemaId = trim((string) ($def['schema_id'] ?? ''));

        if ($schemaId !== '' && function_exists('admin_config_get_value')) {
            $schemaRaw = admin_config_get_value($options, $schemaId);
            if (is_array($schemaRaw)) {
                $schemaRaw = implode(', ', array_map('strval', $schemaRaw));
            }
            $schemaRaw = trim((string) ($schemaRaw === null ? '' : $schemaRaw));
            if ($schemaRaw !== '') {
                return $schemaRaw;
            }
        }

        if ($propLabel !== '' && isset($options['_line_item_properties']) && is_array($options['_line_item_properties'])) {
            foreach ($options['_line_item_properties'] as $k => $v) {
                if (admin_norm_selected_option_label((string) $k) === admin_norm_selected_option_label($propLabel)) {
                    $s = trim((string) $v);
                    if ($s !== '') {
                        return $s;
                    }
                }
            }
        }

        if (isset($options['_selected_summary']) && is_array($options['_selected_summary'])) {
            foreach ($options['_selected_summary'] as $item) {
                if (!is_array($item)) {
                    continue;
                }
                $lab = admin_norm_selected_option_label((string) ($item['label'] ?? ''));
                foreach ($summaryLabels as $candidate) {
                    if ($lab === admin_norm_selected_option_label($candidate)) {
                        $s = trim((string) ($item['value'] ?? ''));
                        if ($s !== '') {
                            return $s;
                        }
                    }
                }
            }
        }

        foreach (admin_resolve_hardware_selections($options) as $row) {
            if (!is_array($row)) {
                continue;
            }
            if ($labelKey !== '') {
                $lbl = trim((string) ($row[$labelKey] ?? ''));
                if ($lbl !== '') {
                    return $lbl;
                }
            }
            if ($valueKey !== '') {
                $val = trim((string) ($row[$valueKey] ?? ''));
                if ($val !== '') {
                    return function_exists('humanize_token') ? humanize_token($val) : $val;
                }
            }
        }

        return '';
    }
}

if (!function_exists('admin_sync_landing_hardware_bidirectional')) {
    /**
     * Keep landing-hardware fields aligned across _hardware_selections, _selected_summary,
     * and _line_item_properties so admin edits and storefront config_id restore match.
     */
    function admin_sync_landing_hardware_bidirectional(array $options) {
        $defs = admin_hardware_landing_field_defs();
        if (empty($defs)) {
            return $options;
        }

        $rows = admin_resolve_hardware_selections($options);
        if (empty($rows)) {
            $rows = [[]];
        }
        $row = is_array($rows[0]) ? $rows[0] : [];
        $changed = false;

        foreach ($defs as $def) {
            if (!is_array($def)) {
                continue;
            }

            // Fields the storefront product does not expose are purged everywhere (row,
            // summary, line-item props, schema keys) so stale values never re-appear or
            // restore on the config_id page. Available fields sync exactly as before.
            if (function_exists('admin_hardware_field_available_for_product')
                && !admin_hardware_field_available_for_product($def, $options)) {
                $removeNeedles = admin_hardware_field_schema_needles($def);
                foreach (['value_key', 'label_key', 'options_key'] as $rk) {
                    $rkey = (string) ($def[$rk] ?? '');
                    if ($rkey !== '' && array_key_exists($rkey, $row)) {
                        unset($row[$rkey]);
                        $changed = true;
                    }
                }
                if (isset($options['_line_item_properties']) && is_array($options['_line_item_properties'])) {
                    foreach (array_keys($options['_line_item_properties']) as $lipKey) {
                        foreach ($removeNeedles as $needle) {
                            if (admin_norm_selected_option_label((string) $lipKey) === admin_norm_selected_option_label($needle)) {
                                unset($options['_line_item_properties'][$lipKey]);
                                break;
                            }
                        }
                    }
                }
                if (isset($options['_selected_summary']) && is_array($options['_selected_summary'])) {
                    $options['_selected_summary'] = array_values(array_filter(
                        $options['_selected_summary'],
                        function ($sumItem) use ($removeNeedles) {
                            if (!is_array($sumItem)) {
                                return true;
                            }
                            $lab = admin_norm_selected_option_label((string) ($sumItem['label'] ?? ''));
                            foreach ($removeNeedles as $needle) {
                                if ($lab === admin_norm_selected_option_label($needle)) {
                                    return false;
                                }
                            }
                            return true;
                        }
                    ));
                }
                $schemaIdRem = (string) ($def['schema_id'] ?? '');
                if ($schemaIdRem !== '') {
                    $normRem = admin_norm_option_id($schemaIdRem);
                    $hyphenRem = str_replace('_', '-', $normRem);
                    foreach (array_unique([$schemaIdRem, $normRem, $hyphenRem]) as $keyRem) {
                        if ($keyRem !== '' && array_key_exists($keyRem, $options)) {
                            unset($options[$keyRem]);
                        }
                    }
                }
                continue;
            }

            $pair = admin_pick_saved_landing_hardware_field_pair($options, $def);
            $raw = (string) ($pair['label'] !== '' ? $pair['label'] : $pair['value']);
            if ($raw === '') {
                continue;
            }

            $propLabel = (string) ($def['prop_label'] ?? '');
            $choices = admin_resolve_field_choices_for_label(
                $options,
                $propLabel !== '' ? $propLabel : (string) ($def['summary_labels'][0] ?? ''),
                [],
                (string) ($def['options_key'] ?? '')
            );
            $resolved = admin_resolve_choice_pair($raw, $choices);
            $machineValue = (string) ($resolved['value'] !== '' ? $resolved['value'] : ($pair['value'] !== '' ? $pair['value'] : $raw));
            $displayLabel = (string) ($resolved['label'] !== '' ? $resolved['label'] : ($pair['label'] !== '' ? $pair['label'] : $raw));

            $valueKey = (string) ($def['value_key'] ?? '');
            $labelKey = (string) ($def['label_key'] ?? '');
            $optionsKey = (string) ($def['options_key'] ?? '');
            $schemaId = (string) ($def['schema_id'] ?? '');

            if ($valueKey !== '') {
                $row[$valueKey] = $machineValue;
                $changed = true;
            }
            if ($labelKey !== '') {
                $row[$labelKey] = $displayLabel;
                $changed = true;
            }
            if ($optionsKey !== '' && !empty($choices)) {
                $row[$optionsKey] = $choices;
                $changed = true;
            }

            if ($schemaId !== '') {
                $options[$schemaId] = $machineValue;
                $norm = admin_norm_option_id($schemaId);
                if ($norm !== $schemaId) {
                    $options[$norm] = $machineValue;
                }
                $hyphen = str_replace('_', '-', $norm);
                if ($hyphen !== $schemaId && $hyphen !== $norm) {
                    $options[$hyphen] = $machineValue;
                }
            }

            if ($propLabel !== '') {
                if (!isset($options['_line_item_properties']) || !is_array($options['_line_item_properties'])) {
                    $options['_line_item_properties'] = [];
                }
                $options['_line_item_properties'][$propLabel] = $displayLabel;

                if (!isset($options['_selected_summary']) || !is_array($options['_selected_summary'])) {
                    $options['_selected_summary'] = [];
                }
                $nl = admin_norm_selected_option_label($propLabel);
                $found = false;
                foreach ($options['_selected_summary'] as &$item) {
                    if (!is_array($item)) {
                        continue;
                    }
                    if (admin_norm_selected_option_label((string) ($item['label'] ?? '')) === $nl) {
                        $item['value'] = $displayLabel;
                        $found = true;
                        break;
                    }
                }
                unset($item);
                if (!$found) {
                    $options['_selected_summary'][] = [
                        'label' => $propLabel,
                        'value' => $displayLabel,
                        'image' => '',
                    ];
                }
            }
        }

        if ($changed) {
            $rows[0] = $row;
            $rows = admin_filter_hardware_selections(admin_normalize_hardware_selections($rows));
            if (!empty($rows)) {
                $options['_hardware_selections'] = $rows;
                $options['hardware_options'] = $rows;
            }
        }

        return $options;
    }
}

if (!function_exists('admin_resolve_hardware_landing_selected_option_rows')) {
    /**
     * Build Selected-options rows for landing-hardware fields from the authoritative
     * saved hardware row (not stale top-level schema keys).
     */
    function admin_resolve_hardware_landing_selected_option_rows(array $options) {
        $rows = [];
        foreach (admin_hardware_landing_field_defs() as $def) {
            if (!is_array($def)) {
                continue;
            }
            $propLabel = (string) ($def['prop_label'] ?? '');
            if ($propLabel === '') {
                continue;
            }
            $pair = admin_pick_saved_landing_hardware_field_pair($options, $def);
            $raw = (string) ($pair['label'] !== '' ? $pair['label'] : $pair['value']);
            if ($raw === '') {
                continue;
            }
            $choices = admin_resolve_field_choices_for_label(
                $options,
                $propLabel,
                [],
                (string) ($def['options_key'] ?? '')
            );
            $resolved = admin_resolve_choice_pair($raw, $choices);
            $rows[] = [
                'source'        => 'hardware',
                'id'            => (string) ($def['schema_id'] ?? ''),
                'label'         => $propLabel,
                'value'         => (string) ($resolved['label'] !== '' ? $resolved['label'] : $raw),
                'storage_value' => (string) ($resolved['value'] !== '' ? $resolved['value'] : ($pair['value'] !== '' ? $pair['value'] : $raw)),
                'choices'       => $choices,
                'image'         => '',
            ];
        }
        return $rows;
    }
}

if (!function_exists('admin_merge_hardware_landing_selected_option_rows')) {
    /** Overwrite / insert hardware landing rows so admin UI matches storefront saves. */
    function admin_merge_hardware_landing_selected_option_rows(array $options, array $rows) {
        $hwRows = admin_resolve_hardware_landing_selected_option_rows($options);
        $hwByLabel = [];
        foreach ($hwRows as $hwRow) {
            if (!is_array($hwRow)) {
                continue;
            }
            $nl = admin_norm_selected_option_label((string) ($hwRow['label'] ?? ''));
            if ($nl !== '') {
                $hwByLabel[$nl] = $hwRow;
            }
        }
        if (empty($hwByLabel)) {
            return $rows;
        }

        $out = [];
        $seen = [];
        foreach ($rows as $row) {
            if (!is_array($row)) {
                continue;
            }
            $nl = admin_norm_selected_option_label((string) ($row['label'] ?? ''));
            if ($nl !== '' && isset($hwByLabel[$nl])) {
                $out[] = $hwByLabel[$nl];
                $seen[$nl] = true;
            } else {
                $out[] = $row;
                if ($nl !== '') {
                    $seen[$nl] = true;
                }
            }
        }
        foreach ($hwRows as $hwRow) {
            if (!is_array($hwRow)) {
                continue;
            }
            $nl = admin_norm_selected_option_label((string) ($hwRow['label'] ?? ''));
            if ($nl !== '' && !isset($seen[$nl])) {
                $out[] = $hwRow;
            }
        }
        return $out;
    }
}

if (!function_exists('admin_normalize_casing_selections')) {
    function admin_normalize_casing_selections($raw) {
        if (!is_array($raw)) {
            return [];
        }
        if (isset($raw['product_id']) || isset($raw['title'])) {
            $raw = [$raw];
        }
        $out = [];
        foreach ($raw as $item) {
            if (!is_array($item)) {
                continue;
            }
            if (empty($item['product_id']) && empty($item['title'])) {
                continue;
            }
            $out[] = $item;
        }
        return $out;
    }
}

if (!function_exists('admin_get_casing_selections')) {
    /** Read explicitly stored casing rows only (no Shopify summary/property merge). */
    function admin_get_casing_selections(array $options) {
        $raw = $options['_casing_selections']
            ?? $options['_casing_selection']
            ?? $options['casing_options']
            ?? null;
        if ($raw === null || $raw === '') {
            return [];
        }
        if (is_string($raw)) {
            $decoded = json_decode($raw, true);
            if (is_array($decoded)) {
                $raw = $decoded;
            } else {
                return [['product_id' => $raw, 'title' => $raw]];
            }
        }
        return admin_deduplicate_casing_selections(admin_normalize_casing_selections($raw));
    }
}

if (!function_exists('admin_casing_normalize_title_for_match')) {
    function admin_casing_normalize_title_for_match(array $row) {
        $title = trim((string) ($row['title'] ?? $row['product_id'] ?? ''));
        $sku = trim((string) ($row['sku'] ?? ''));
        if ($sku !== '' && stripos($title, $sku) !== false) {
            $title = trim((string) preg_replace('/\s*\/\s*' . preg_quote($sku, '/') . '\s*$/i', '', $title));
        }
        $title = strtolower($title);
        $title = preg_replace('/\s+/', ' ', $title);
        return $title;
    }
}

if (!function_exists('admin_casing_rows_match')) {
    function admin_casing_rows_match(array $a, array $b) {
        $pidA = trim((string) ($a['product_id'] ?? $a['id'] ?? ''));
        $pidB = trim((string) ($b['product_id'] ?? $b['id'] ?? ''));
        if ($pidA !== '' && $pidB !== '' && $pidA === $pidB) {
            return true;
        }
        $skuA = strtolower(trim((string) ($a['sku'] ?? '')));
        $skuB = strtolower(trim((string) ($b['sku'] ?? '')));
        if ($skuA !== '' && $skuB !== '' && $skuA === $skuB) {
            return true;
        }
        $titleA = admin_casing_normalize_title_for_match($a);
        $titleB = admin_casing_normalize_title_for_match($b);
        return $titleA !== '' && $titleA === $titleB;
    }
}

if (!function_exists('admin_deduplicate_casing_selections')) {
    function admin_deduplicate_casing_selections(array $items) {
        $out = [];
        foreach ($items as $idx => $row) {
            if (!is_array($row)) {
                continue;
            }
            $merged = false;
            foreach ($out as $oi => $existing) {
                if (!admin_casing_rows_match($existing, $row)) {
                    continue;
                }
                $out[$oi] = admin_merge_casing_selection_rows($existing, $row);
                $merged = true;
                break;
            }
            if (!$merged) {
                $out[] = $row;
            }
        }
        return admin_normalize_casing_selections($out);
    }
}

if (!function_exists('admin_dedupe_schema_option_entries')) {
    function admin_dedupe_schema_option_entries(array $schema, $optionId) {
        $target = strtolower(str_replace('-', '_', (string) $optionId));
        $seen = false;
        $out = [];
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id'])) {
                $out[] = $opt;
                continue;
            }
            $id = strtolower(str_replace('-', '_', (string) $opt['id']));
            if ($id === $target) {
                if ($seen) {
                    continue;
                }
                $seen = true;
            }
            $out[] = $opt;
        }
        return $out;
    }
}

if (!function_exists('admin_casing_selection_row_key')) {
    function admin_casing_selection_row_key(array $row, $index = 0) {
        $pid = trim((string) ($row['product_id'] ?? $row['id'] ?? ''));
        if ($pid !== '') {
            return 'id:' . $pid;
        }
        $sku = strtolower(trim((string) ($row['sku'] ?? '')));
        if ($sku !== '') {
            return 'sku:' . $sku;
        }
        $handle = trim((string) ($row['handle'] ?? ''));
        if ($handle !== '') {
            return 'handle:' . $handle;
        }
        $title = admin_casing_normalize_title_for_match($row);
        if ($title !== '') {
            return 'title:' . $title;
        }
        return 'idx:' . (int) $index;
    }
}

if (!function_exists('admin_merge_casing_selection_rows')) {
    /** Overlay fills only empty fields on the base row (existing admin/storefront data wins). */
    function admin_merge_casing_selection_rows(array $base, array $overlay) {
        $out = $base;
        foreach ($overlay as $k => $v) {
            if ($v === null || $v === '') {
                continue;
            }
            if (!array_key_exists($k, $out) || $out[$k] === null || $out[$k] === '') {
                $out[$k] = $v;
            }
        }
        return $out;
    }
}

if (!function_exists('admin_merge_casing_selection_streams')) {
    /**
     * Merge multiple casing sources (Shopify summary, line properties, saved JSON).
     *
     * @param array<int, array<int, array>> $streams lowest priority first
     */
    function admin_merge_casing_selection_streams(array $streams) {
        $out = [];
        foreach ($streams as $stream) {
            if (!is_array($stream)) {
                continue;
            }
            foreach ($stream as $idx => $row) {
                if (!is_array($row)) {
                    continue;
                }
                $key = admin_casing_selection_row_key($row, $idx);
                if (!isset($out[$key])) {
                    $out[$key] = $row;
                    continue;
                }
                $out[$key] = admin_merge_casing_selection_rows($out[$key], $row);
            }
        }
        return admin_deduplicate_casing_selections(array_values($out));
    }
}

if (!function_exists('admin_parse_casing_summary_text')) {
    function admin_parse_casing_summary_text($value, $image = '') {
        $value = trim((string) $value);
        if ($value === '') {
            return [];
        }
        $row = [
            'title' => $value,
            'qty' => 1,
        ];
        if ($image !== '') {
            $row['image'] = $image;
        }
        if (preg_match('/\(×(\d+)\)\s*$/u', $value, $m)) {
            $row['qty'] = max(1, (int) $m[1]);
            $value = trim((string) preg_replace('/\(×\d+\)\s*$/u', '', $value));
        }
        if (preg_match('/\(([^)]+)\)\s*$/u', $value, $m) && strpos($m[1], '$') !== false) {
            $row['price_formatted'] = trim($m[1]);
            $value = trim((string) preg_replace('/\([^)]+\)\s*$/u', '', $value));
        }
        if (preg_match('/ — (.+)$/u', $value, $m)) {
            $len = trim($m[1]);
            $row['length_label'] = $len;
            $row['length_value'] = $len;
            $value = trim((string) preg_replace('/ — .+$/u', '', $value));
        }
        if (preg_match('/^(.+?) \/ (.+)$/u', $value, $m)) {
            $row['title'] = trim($m[1]);
            $row['sku'] = trim($m[2]);
        } else {
            $row['title'] = $value;
        }
        return $row;
    }
}

if (!function_exists('admin_text_looks_like_casing_product')) {
    function admin_text_looks_like_casing_product($text) {
        $text = trim((string) $text);
        if ($text === '') {
            return false;
        }
        if (preg_match('/\bcasing\b/i', $text)) {
            return true;
        }
        if (preg_match('/\b(?:IC\d+|EC\d+|CT\d+)\b/i', $text)) {
            return true;
        }
        if (preg_match('/\b(?:interior|exterior)\s+casing\b/i', $text)) {
            return true;
        }
        return false;
    }
}

if (!function_exists('admin_casing_selections_from_selected_summary')) {
    function admin_casing_selections_from_selected_summary(array $options) {
        $summary = $options['_selected_summary'] ?? null;
        if (!is_array($summary)) {
            return [];
        }
        $out = [];
        foreach ($summary as $item) {
            if (!is_array($item)) {
                continue;
            }
            $label = trim((string) ($item['label'] ?? ''));
            $value = trim((string) ($item['value'] ?? ''));
            if ($value === '') {
                continue;
            }
            $labelIsCasing = $label !== '' && stripos($label, 'casing') !== false;
            $valueIsCasing = admin_text_looks_like_casing_product($value);
            if (!$labelIsCasing && !$valueIsCasing) {
                continue;
            }
            $parsed = admin_parse_casing_summary_text($value, (string) ($item['image'] ?? ''));
            if (!empty($parsed)) {
                $out[] = $parsed;
            }
        }
        return admin_normalize_casing_selections($out);
    }
}

if (!function_exists('admin_casing_selections_from_options_summary_text')) {
    function admin_casing_selections_from_options_summary_text(array $options) {
        $text = trim((string) ($options['options_summary'] ?? $options['_options_summary'] ?? ''));
        if ($text === '') {
            return [];
        }
        $out = [];
        foreach (preg_split('/;\s*/', $text) as $part) {
            $part = trim($part);
            if ($part === '' || !preg_match('/^([^:]+):(.+)$/', $part, $m)) {
                continue;
            }
            $label = trim($m[1]);
            $value = trim($m[2]);
            if ($label === '' || $value === '') {
                continue;
            }
            if (stripos($label, 'casing') === false && !admin_text_looks_like_casing_product($value)) {
                continue;
            }
            $parsed = admin_parse_casing_summary_text($value);
            if (!empty($parsed)) {
                $out[] = $parsed;
            }
        }
        return admin_normalize_casing_selections($out);
    }
}

if (!function_exists('admin_casing_selections_from_product_id_fields')) {
    function admin_casing_selections_from_product_id_fields(array $options) {
        $raw = $options['casing_options'] ?? $options['Casing'] ?? null;
        if ($raw === null || $raw === '' || is_array($raw)) {
            return [];
        }
        $catalog = admin_get_recommended_casing_catalog($options);
        $pid = trim((string) $raw);
        if ($pid === '') {
            return [];
        }
        $row = ['product_id' => $pid, 'title' => $pid];
        if (isset($catalog[$pid])) {
            $row = admin_hydrate_casing_selection($row, $catalog);
        }
        return admin_normalize_casing_selections([$row]);
    }
}

if (!function_exists('admin_casing_selections_from_line_properties')) {
    function admin_casing_selections_from_line_properties(array $options) {
        $props = [];
        if (isset($options['_line_item_properties']) && is_array($options['_line_item_properties'])) {
            foreach ($options['_line_item_properties'] as $k => $v) {
                if (is_string($k) && $k !== '' && is_scalar($v) && trim((string) $v) !== '') {
                    $props[$k] = (string) $v;
                }
            }
        }
        foreach (['Casing', 'casing_options'] as $key) {
            if (!empty($options[$key]) && is_scalar($options[$key])) {
                $props[$key] = (string) $options[$key];
            }
        }
        $out = [];
        foreach ($props as $label => $value) {
            if (stripos($label, 'casing') === false) {
                continue;
            }
            if ($value !== '' && $value[0] === '{') {
                $decoded = json_decode($value, true);
                if (is_array($decoded)) {
                    foreach (admin_normalize_casing_selections($decoded) as $row) {
                        $out[] = $row;
                    }
                    continue;
                }
            }
            $parsed = admin_parse_casing_summary_text($value);
            if (!empty($parsed)) {
                $out[] = $parsed;
            }
        }
        return admin_normalize_casing_selections($out);
    }
}

if (!function_exists('admin_get_recommended_casing_catalog')) {
    function admin_get_recommended_casing_catalog(array $options) {
        $raw = $options['_recommended_casing_products']
            ?? $options['recommended_casing_products']
            ?? null;
        if (!is_array($raw)) {
            return [];
        }
        $out = [];
        foreach ($raw as $product) {
            if (!is_array($product)) {
                continue;
            }
            $pid = trim((string) ($product['id'] ?? $product['product_id'] ?? ''));
            if ($pid === '') {
                continue;
            }
            $out[$pid] = $product;
            $handle = trim((string) ($product['handle'] ?? ''));
            if ($handle !== '') {
                $out['handle:' . $handle] = $product;
            }
        }
        return $out;
    }
}

if (!function_exists('admin_hydrate_casing_selection')) {
    function admin_hydrate_casing_selection(array $item, array $catalog) {
        if (empty($catalog)) {
            return $item;
        }
        $product = null;
        $pid = trim((string) ($item['product_id'] ?? $item['id'] ?? ''));
        if ($pid !== '' && isset($catalog[$pid])) {
            $product = $catalog[$pid];
        }
        if ($product === null) {
            $handle = trim((string) ($item['handle'] ?? ''));
            if ($handle !== '' && isset($catalog['handle:' . $handle])) {
                $product = $catalog['handle:' . $handle];
            }
        }
        if (!is_array($product)) {
            return $item;
        }
        $overlay = [
            'product_id' => (string) ($product['id'] ?? $product['product_id'] ?? ($item['product_id'] ?? '')),
            'handle' => (string) ($product['handle'] ?? ($item['handle'] ?? '')),
            'title' => (string) ($product['title'] ?? ($item['title'] ?? '')),
            'url' => (string) ($product['url'] ?? ($item['url'] ?? '')),
            'variant_id' => (string) ($product['variant_id'] ?? ($item['variant_id'] ?? '')),
            'sku' => (string) ($product['sku'] ?? ($item['sku'] ?? '')),
            'image' => (string) ($product['image'] ?? ($item['image'] ?? '')),
            'price_cents' => $product['price_cents'] ?? ($item['price_cents'] ?? null),
            'price_formatted' => (string) ($product['price_formatted'] ?? ($item['price_formatted'] ?? '')),
        ];
        if (!empty($product['length_options']) && is_array($product['length_options'])) {
            $overlay['length_options'] = $product['length_options'];
        }
        return admin_merge_casing_selection_rows($item, array_filter($overlay, function ($v) {
            return $v !== null && $v !== '';
        }));
    }
}

if (!function_exists('admin_hydrate_casing_selections')) {
    function admin_hydrate_casing_selections(array $items, array $catalog) {
        if (empty($catalog)) {
            return $items;
        }
        $out = [];
        foreach ($items as $item) {
            if (!is_array($item)) {
                continue;
            }
            $out[] = admin_hydrate_casing_selection($item, $catalog);
        }
        return $out;
    }
}

if (!function_exists('admin_resolve_casing_selections')) {
    /**
     * Merge Shopify storefront data (summary/properties) with saved casing JSON.
     * Safe no-op when no casing data exists anywhere.
     */
    function admin_resolve_casing_selections(array $options) {
        $catalog = admin_get_recommended_casing_catalog($options);
        $structured = admin_get_casing_selections($options);
        if (!empty($structured)) {
            return admin_deduplicate_casing_selections(
                admin_hydrate_casing_selections($structured, $catalog)
            );
        }
        $streams = [
            admin_casing_selections_from_options_summary_text($options),
            admin_casing_selections_from_line_properties($options),
            admin_casing_selections_from_selected_summary($options),
            admin_casing_selections_from_product_id_fields($options),
        ];
        $merged = admin_merge_casing_selection_streams($streams);
        if (empty($merged)) {
            return [];
        }
        return admin_deduplicate_casing_selections(
            admin_hydrate_casing_selections($merged, $catalog)
        );
    }
}

if (!function_exists('admin_merge_shopify_casing_into_options')) {
    function admin_merge_shopify_casing_into_options(array $options) {
        $resolved = admin_resolve_casing_selections($options);
        if (empty($resolved)) {
            return $options;
        }
        $options['_casing_selections'] = $resolved;
        $options['casing_options'] = $resolved;
        if (count($resolved) === 1) {
            $options['_casing_selection'] = $resolved[0];
        }
        return $options;
    }
}

if (!function_exists('admin_merge_casing_admin_post')) {
    /** Preserve image/title/price from saved rows when admin edits length/qty only. */
    function admin_merge_casing_admin_post(array $existing, array $posted) {
        $existing = admin_normalize_casing_selections($existing);
        $posted = admin_normalize_casing_selections($posted);
        if (empty($posted)) {
            return [];
        }
        if (empty($existing)) {
            return $posted;
        }
        $byId = [];
        foreach ($existing as $row) {
            if (!is_array($row)) {
                continue;
            }
            $byId[admin_casing_selection_row_key($row)] = $row;
        }
        $out = [];
        foreach ($posted as $idx => $row) {
            if (!is_array($row)) {
                continue;
            }
            $base = $byId[admin_casing_selection_row_key($row, $idx)] ?? ($existing[$idx] ?? []);
            $out[] = array_merge(is_array($base) ? $base : [], $row);
        }
        return admin_normalize_casing_selections($out);
    }
}

if (!function_exists('admin_merge_incoming_configuration_options')) {
    /**
     * Merge storefront/API options into a saved configuration without dropping casing data.
     * Drop-in replacement for configurations_api_merge_options() in api/configurations.php.
     */
    function admin_merge_incoming_configuration_options(array $existing, array $incoming) {
        // Measurement dimensions are stored under several alias keys (e.g. exact_door_width
        // vs exact_door_width_int vs door_width). A storefront edit may send the NEW value
        // under one alias while a stale value lingers under another; since the admin reads
        // the first non-empty alias, the old value would shadow the new one. So whenever an
        // incoming key belongs to a measurement alias group, drop the OTHER aliases of that
        // group from $existing before applying the incoming values.
        if (function_exists('admin_measurement_alias_groups') && function_exists('admin_norm_option_id')) {
            $groups = admin_measurement_alias_groups();
            foreach ($incoming as $ik => $iv) {
                if (!is_string($ik) || $ik === '' || $ik[0] === '_') {
                    continue;
                }
                $inNorm = admin_norm_option_id($ik);
                foreach ($groups as $group) {
                    $groupNorm = array_map('admin_norm_option_id', $group);
                    if (!in_array($inNorm, $groupNorm, true)) {
                        continue;
                    }
                    foreach (array_keys($existing) as $ek) {
                        if (!is_string($ek) || $ek === '') {
                            continue;
                        }
                        $ekNorm = admin_norm_option_id($ek);
                        if ($ekNorm === $inNorm) {
                            continue; // keep the alias the incoming value will overwrite
                        }
                        if (in_array($ekNorm, $groupNorm, true)) {
                            unset($existing[$ek]);
                        }
                    }
                }
            }
        }
        foreach ($incoming as $k => $v) {
            $existing[$k] = $v;
        }
        $merged = admin_merge_shopify_casing_into_options($existing);
        return admin_sync_listing_selected_summary($merged);
    }
}

if (!function_exists('admin_casing_length_choices_for_item')) {
  /**
   * Length dropdown choices for one saved casing line (storefront selection or admin edit).
   *
   * @return array<int, array{value: string, label: string}>
   */
    function admin_casing_length_choices_for_item(array $item) {
        $choices = [];
        if (!empty($item['length_options']) && is_array($item['length_options'])) {
            foreach ($item['length_options'] as $lo) {
                if (!is_array($lo)) {
                    continue;
                }
                $v = trim((string) ($lo['value'] ?? $lo['label'] ?? ''));
                $l = trim((string) ($lo['label'] ?? $lo['value'] ?? ''));
                if ($v === '' && $l === '') {
                    continue;
                }
                $choices[] = [
                    'value' => $v !== '' ? $v : $l,
                    'label' => $l !== '' ? $l : $v,
                ];
            }
        }
        $curVal = trim((string) ($item['length_value'] ?? ''));
        $curLbl = trim((string) ($item['length_label'] ?? $curVal));
        if ($curVal !== '' || $curLbl !== '') {
            $found = false;
            foreach ($choices as $c) {
                if (($c['value'] ?? '') === $curVal || ($c['label'] ?? '') === $curLbl) {
                    $found = true;
                    break;
                }
            }
            if (!$found) {
                array_unshift($choices, [
                    'value' => $curVal !== '' ? $curVal : $curLbl,
                    'label' => $curLbl !== '' ? $curLbl : $curVal,
                ]);
            }
        }
        if (empty($choices)) {
            foreach (["7'", "8'", "10'", "12'"] as $std) {
                $choices[] = ['value' => $std, 'label' => $std];
            }
        }
        return $choices;
    }
}

if (!function_exists('admin_casing_line_label')) {
    function admin_casing_line_label(array $item) {
        $t = trim((string)($item['title'] ?? $item['product_id'] ?? ''));
        if ($t === '') {
            return '';
        }
        $sku = trim((string)($item['sku'] ?? ''));
        if ($sku !== '' && stripos($t, $sku) === false) {
            $t .= ' / ' . $sku;
        }
        $len = trim((string)($item['length_label'] ?? $item['length_value'] ?? ''));
        if ($len !== '') {
            $t .= ' — ' . $len;
        }
        $price = trim((string)($item['price_formatted'] ?? ''));
        if ($price !== '' && stripos($t, $price) === false) {
            $t .= ' (' . $price . ')';
        }
        $q = (int)($item['qty'] ?? 1);
        if ($q > 1) {
            $t .= ' (×' . $q . ')';
        }
        return $t;
    }
}

if (!function_exists('admin_casing_summary_parts')) {
    function admin_casing_summary_parts(array $options) {
        $parts = [];
        foreach (admin_resolve_casing_selections($options) as $item) {
            $line = admin_casing_line_label($item);
            if ($line !== '') {
                $parts[] = 'Casing: ' . $line;
            }
        }
        return $parts;
    }
}

if (!function_exists('admin_ensure_casing_schema')) {
    /**
     * Inject casing_options into the admin schema only when the storefront saved an
     * explicit casing pick (_casing_selections / casing_options). Otherwise strip
     * casing_options so the empty "Casing" accordion is not shown on every edit page.
     *
     * @param array|null $storefrontCasing Pre-merge rows from admin_get_casing_selections()
     *                                    (avoids treating heuristic merge data as a pick).
     */
    function admin_ensure_casing_schema(array $schema, array $options, $storefrontCasing = null) {
        $schema = admin_dedupe_schema_option_entries($schema, 'casing_options');
        $stored = is_array($storefrontCasing)
            ? admin_deduplicate_casing_selections(admin_normalize_casing_selections($storefrontCasing))
            : admin_get_casing_selections($options);
        if (empty($stored)) {
            $out = [];
            foreach ($schema as $opt) {
                if (!is_array($opt) || empty($opt['id'])) {
                    $out[] = $opt;
                    continue;
                }
                if (strtolower(str_replace('-', '_', (string) $opt['id'])) === 'casing_options') {
                    continue;
                }
                $out[] = $opt;
            }
            return $out;
        }
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id'])) {
                continue;
            }
            if (strtolower(str_replace('-', '_', (string) $opt['id'])) === 'casing_options') {
                return $schema;
            }
        }
        $schema[] = [
            'id' => 'casing_options',
            'label' => 'Casing',
            'type' => 'casing_multi',
            '_casingSelections' => true,
        ];
        return $schema;
    }
}

if (!function_exists('admin_ensure_hardware_schema')) {
    /**
     * When options_json has hardware picks but no _pricing_schema (hardware-only saves),
     * inject a hardware_multi stub so the admin edit page renders editable dropdowns.
     */
    function admin_ensure_hardware_schema(array $schema, array $options) {
        $schema = admin_dedupe_schema_option_entries($schema, 'hardware_options');
        $stored = admin_resolve_hardware_selections($options);
        if (empty($stored)) {
            $out = [];
            foreach ($schema as $opt) {
                if (!is_array($opt) || empty($opt['id'])) {
                    $out[] = $opt;
                    continue;
                }
                if (strtolower(str_replace('-', '_', (string) $opt['id'])) === 'hardware_options') {
                    continue;
                }
                $out[] = $opt;
            }
            return $out;
        }
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id'])) {
                continue;
            }
            if (strtolower(str_replace('-', '_', (string) $opt['id'])) === 'hardware_options') {
                return $schema;
            }
        }
        $schema[] = [
            'id'    => 'hardware_options',
            'label' => 'Hardware',
            'type'  => 'hardware_multi',
        ];
        return $schema;
    }
}

if (!function_exists('admin_hydrate_hardware_selections_from_summary')) {
    /**
     * Copy saved _selected_summary / _line_item_properties values onto the hardware row
     * when landing-hardware fields were not stored on _hardware_selections (simple sync).
     */
    function admin_hydrate_hardware_selections_from_summary(array $options) {
        $rows = admin_resolve_hardware_selections($options);
        if (empty($rows) || !is_array($rows[0])) {
            return $options;
        }
        $row = $rows[0];
        $changed = false;

        foreach (admin_hardware_landing_field_defs() as $def) {
            if (!is_array($def)) {
                continue;
            }
            $vk = (string) ($def['value_key'] ?? '');
            $lk = (string) ($def['label_key'] ?? '');
            $prop = (string) ($def['prop_label'] ?? '');
            if ($vk === '' && $lk === '') {
                continue;
            }
            $hasVal = $vk !== '' && trim((string) ($row[$vk] ?? '')) !== '';
            $hasLbl = $lk !== '' && trim((string) ($row[$lk] ?? '')) !== '';
            if ($hasVal && $hasLbl) {
                continue;
            }

            $saved = '';
            if ($prop !== '' && isset($options['_line_item_properties']) && is_array($options['_line_item_properties'])) {
                foreach ($options['_line_item_properties'] as $k => $v) {
                    if (admin_norm_selected_option_label((string) $k) === admin_norm_selected_option_label($prop)) {
                        $saved = trim((string) $v);
                        break;
                    }
                }
            }
            if ($saved === '' && isset($options['_selected_summary']) && is_array($options['_selected_summary'])) {
                foreach ($options['_selected_summary'] as $item) {
                    if (!is_array($item)) {
                        continue;
                    }
                    if (admin_norm_selected_option_label((string) ($item['label'] ?? '')) !== admin_norm_selected_option_label($prop)) {
                        continue;
                    }
                    $saved = trim((string) ($item['value'] ?? ''));
                    break;
                }
            }
            if ($saved === '') {
                continue;
            }
            if ($lk !== '' && !$hasLbl) {
                $row[$lk] = $saved;
                $changed = true;
            }
            if ($vk !== '' && !$hasVal) {
                $row[$vk] = $saved;
                $changed = true;
            }
        }

        if ($changed) {
            $rows[0] = $row;
            $options['_hardware_selections'] = $rows;
            $options['hardware_options'] = $rows;
        }
        return $options;
    }
}

if (!function_exists('admin_filter_hardware_from_selected_option_fields')) {
    /** Drop hardware landing rows from Selected options when the hardware card edits them. */
    function admin_filter_hardware_from_selected_option_fields(array $fields, array $options) {
        if (empty(admin_resolve_hardware_selections($options))) {
            return $fields;
        }
        $out = [];
        foreach ($fields as $field) {
            if (!is_array($field)) {
                continue;
            }
            if (admin_is_hardware_landing_field_label((string) ($field['label'] ?? ''))) {
                continue;
            }
            $out[] = $field;
        }
        return $out;
    }
}

if (!function_exists('admin_enrich_selected_option_field_values')) {
    /** Fill missing display/storage values so dropdowns are never blank. */
    function admin_enrich_selected_option_field_values(array $field) {
        if (!is_array($field)) {
            return $field;
        }
        $value = trim((string) ($field['value'] ?? ''));
        $storage = trim((string) ($field['storage_value'] ?? ''));
        if ($value === '' && $storage !== '') {
            $field['value'] = function_exists('humanize_token') ? humanize_token($storage) : $storage;
        } elseif ($storage === '' && $value !== '') {
            $field['storage_value'] = $value;
        }
        $field['choices'] = admin_ensure_current_choice_in_list(
            isset($field['choices']) && is_array($field['choices']) ? $field['choices'] : [],
            (string) ($field['storage_value'] ?? ''),
            (string) ($field['value'] ?? '')
        );
        return $field;
    }
}

if (!function_exists('admin_count_renderable_selected_option_choices')) {
    function admin_count_renderable_selected_option_choices(array $choices) {
        $n = 0;
        foreach (admin_normalize_choice_list($choices) as $c) {
            $cv = trim((string) ($c['value'] ?? ''));
            $cl = trim((string) ($c['label'] ?? ''));
            if ($cv !== '' || $cl !== '') {
                $n++;
            }
        }
        return $n;
    }
}

if (!function_exists('admin_norm_selected_option_label')) {
    /** Normalize a "selected option" label for matching across schema / summary / line props. */
    function admin_norm_selected_option_label($s) {
        $s = strtolower(trim((string) $s));
        $s = preg_replace('/\s+/', ' ', $s);
        return $s === null ? '' : $s;
    }
}

if (!function_exists('admin_selected_option_field_is_handled_type')) {
    /** True for option types already rendered by the main editor loop / special sections. */
    function admin_selected_option_field_is_handled_type($type) {
        return in_array((string) $type, ['select', 'radio', 'checkbox', 'number', 'dimension', 'hardware_multi', 'casing_multi'], true);
    }
}

if (!function_exists('admin_millwork_selected_option_choices')) {
    /**
     * Editable choice catalog for the "Selected options" dropdowns (millwork & similar
     * products that save no _pricing_schema). Keyed by NORMALIZED label
     * (lowercase, single-spaced). Each entry is a list of ['value' => ..., 'label' => ...].
     *
     * value === label here because these products store the display string in
     * _selected_summary. To add a field as a dropdown, just add its options below.
     * Any label NOT in this catalog (and not carrying schema choices) renders as an
     * editable text input instead.
     */
    function admin_millwork_selected_option_choices() {
        $simple = function (array $labels) {
            $out = [];
            foreach ($labels as $l) {
                $l = (string) $l;
                if ($l === '') continue;
                $out[] = ['value' => $l, 'label' => $l];
            }
            return $out;
        };
        // value => label pairs (machine value differs from the human label).
        $pairs = function (array $map) {
            $out = [];
            foreach ($map as $value => $label) {
                $out[] = ['value' => (string) $value, 'label' => (string) $label];
            }
            return $out;
        };

        // Length: 1 to 100 (value === label) for numeric custom-length products.
        $lengths = [];
        for ($i = 1; $i <= 100; $i++) $lengths[] = (string) $i;
        $balusterLengths = function_exists('admin_millwork_default_length_choice_labels')
            ? admin_millwork_default_length_choice_labels()
            : ["7'", "8'", "10'", "12'"];

        return [
            // Values mirror the storefront baluster/millwork product page dropdowns.
            // Order is kept identical to the front end. Edit these lists to match the
            // storefront exactly if options are added/removed there.
            'wood' => $simple([
                'African Mahogany',
                'Honduran Mahogany',
                'Spanish Cedar',
                'Vertical Grain Douglas Fir',
            ]),
            'custom length' => $simple($lengths),
            'millwork length' => $simple($balusterLengths),
            // Thickness mirrors the storefront <select data-millwork-custom-thickness-select>:
            // option value is the decimal (matches saved custom_thickness), the label is the
            // fraction (matches _selected_summary). Order kept identical to the front end.
            'custom thickness' => $pairs([
                '0'      => '0',
                '0.0625' => '1/16',
                '0.125'  => '1/8',
                '0.25'   => '1/4',
                '0.5'    => '1/2',
                '0.1875' => '3/16',
                '0.375'  => '3/8',
                '0.75'   => '3/4',
                '0.3125' => '5/16',
                '0.625'  => '5/8',
                '0.4375' => '7/16',
                '0.875'  => '7/8',
                '0.5625' => '9/16',
                '0.6875' => '11/16',
                '0.8125' => '13/16',
                '0.9375' => '15/16',
            ]),
        ];
    }
}

if (!function_exists('admin_millwork_default_length_choice_labels')) {
    /** Default baluster / millwork length labels mirrored from the storefront dropdown. */
    function admin_millwork_default_length_choice_labels() {
        return ["7'", "8'", "10'", "12'"];
    }
}

if (!function_exists('admin_is_millwork_length_field')) {
    /** True when a selected-options row refers to millwork length (not door measurements). */
    function admin_is_millwork_length_field($id, $label) {
        if (admin_norm_option_id($id) === 'custom_length') {
            return true;
        }
        $nl = admin_norm_selected_option_label($label);
        return in_array($nl, ['custom length', 'millwork length', 'length'], true);
    }
}

if (!function_exists('admin_length_choices_from_option_list')) {
    /**
     * @param mixed $list
     * @return list<array{value: string, label: string}>
     */
    function admin_length_choices_from_option_list($list) {
        if (!is_array($list) || empty($list)) {
            return [];
        }
        $choices = [];
        foreach ($list as $lo) {
            if (is_scalar($lo)) {
                $s = trim((string) $lo);
                if ($s === '') continue;
                $choices[] = ['value' => $s, 'label' => $s];
                continue;
            }
            if (!is_array($lo)) continue;
            $v = trim((string) ($lo['value'] ?? $lo['label'] ?? ''));
            $l = trim((string) ($lo['label'] ?? $lo['value'] ?? ''));
            if ($v === '' && $l === '') continue;
            $choices[] = [
                'value' => $v !== '' ? $v : $l,
                'label' => $l !== '' ? $l : $v,
            ];
        }
        return $choices;
    }
}

if (!function_exists('admin_resolve_millwork_length_choices')) {
    /**
     * Choice list for millwork length fields: live storefront first, then saved
     * product length_options, then static catalog, then baluster defaults.
     *
     * @return list<array{value: string, label: string}>
     */
    function admin_resolve_millwork_length_choices($label, array $options, array $liveMap, array $catalog) {
        $live = admin_match_choices_for_label($label, $liveMap);
        if (!empty($live)) {
            return $live;
        }

        if (!empty($options['length_options'])) {
            $saved = admin_length_choices_from_option_list($options['length_options']);
            if (!empty($saved)) {
                return $saved;
            }
        }

        $nl = admin_norm_selected_option_label($label);
        if ($nl !== '' && isset($catalog[$nl]) && is_array($catalog[$nl]) && !empty($catalog[$nl])) {
            return $catalog[$nl];
        }

        $fromCatalog = admin_match_choices_for_label($label, $catalog);
        if (!empty($fromCatalog)) {
            return $fromCatalog;
        }

        if ($nl === 'custom length' && isset($catalog['custom length']) && is_array($catalog['custom length'])) {
            return $catalog['custom length'];
        }

        if ($nl === 'millwork length') {
            $baluster = admin_millwork_default_length_choice_labels();
            $out = [];
            foreach ($baluster as $len) {
                $out[] = ['value' => (string) $len, 'label' => (string) $len];
            }
            return $out;
        }

        return [];
    }
}

if (!function_exists('admin_is_custom_thickness_field')) {
  /** True when a selected-options row refers to millwork custom thickness. */
  function admin_is_custom_thickness_field($id, $label) {
    if (admin_norm_option_id($id) === 'custom_thickness') {
      return true;
    }
    $nl = admin_norm_selected_option_label($label);
    return $nl === 'custom thickness' || $nl === 'thickness';
  }
}

if (!function_exists('admin_millwork_thickness_choice_pairs')) {
  /** @return array<string, string> decimal storage value => fraction display label */
  function admin_millwork_thickness_choice_pairs() {
    $catalog = function_exists('admin_millwork_selected_option_choices')
      ? admin_millwork_selected_option_choices()
      : [];
    $choices = (isset($catalog['custom thickness']) && is_array($catalog['custom thickness']))
      ? $catalog['custom thickness']
      : [];
    $pairs = [];
    foreach ($choices as $c) {
      if (!is_array($c)) continue;
      $cv = trim((string) ($c['value'] ?? ''));
      $cl = trim((string) ($c['label'] ?? $cv));
      if ($cv === '' && $cl === '') continue;
      $pairs[$cv !== '' ? $cv : $cl] = $cl !== '' ? $cl : $cv;
    }
    return $pairs;
  }
}

if (!function_exists('admin_normalize_millwork_thickness_storage_value')) {
  /**
   * Resolve admin/storefront thickness input (decimal or fraction label) to the
   * decimal value stored in custom_thickness on the storefront.
   */
  function admin_normalize_millwork_thickness_storage_value($value) {
    $value = trim((string) $value);
    if ($value === '') return '';
    foreach (admin_millwork_thickness_choice_pairs() as $storage => $label) {
      if ($value === $storage || $value === $label) {
        return $storage;
      }
      if (is_numeric($value) && is_numeric($storage) && abs((float) $value - (float) $storage) < 0.000001) {
        return $storage;
      }
    }
    return $value;
  }
}

if (!function_exists('admin_millwork_thickness_display_label')) {
  /** Human fraction label for a stored custom_thickness decimal (falls back to raw). */
  function admin_millwork_thickness_display_label($storageValue) {
    $storageValue = trim((string) $storageValue);
    if ($storageValue === '') return '';
    foreach (admin_millwork_thickness_choice_pairs() as $storage => $label) {
      if ($storageValue === $storage) {
        return $label;
      }
      if (is_numeric($storageValue) && is_numeric($storage) && abs((float) $storageValue - (float) $storage) < 0.000001) {
        return $label;
      }
    }
    return $storageValue;
  }
}

if (!function_exists('admin_infer_millwork_selected_option_id')) {
  /** Map a summary label to its storefront option id when known (millwork / hardware). */
  function admin_infer_millwork_selected_option_id($label) {
    $nl = admin_norm_selected_option_label($label);
    if ($nl === 'custom thickness' || $nl === 'thickness') return 'custom_thickness';
    if (in_array($nl, ['custom length', 'millwork length', 'length'], true)) return 'custom_length';
    if (in_array($nl, ['wood', 'millwork wood', 'wood species'], true)) return 'millwork_wood';
    $hw = admin_infer_hardware_selected_option_id($label);
    return $hw !== '' ? $hw : '';
  }
}

if (!function_exists('admin_config_summary_has_millwork_wood')) {
    /** True when _selected_summary explicitly stores a Millwork Wood value. */
    function admin_config_summary_has_millwork_wood(array $options) {
        $summary = (isset($options['_selected_summary']) && is_array($options['_selected_summary']))
            ? $options['_selected_summary'] : [];
        foreach ($summary as $item) {
            if (!is_array($item)) {
                continue;
            }
            $nl = admin_norm_selected_option_label((string) ($item['label'] ?? ''));
            $value = trim((string) ($item['value'] ?? ''));
            if ($nl === 'millwork wood' && $value !== '') {
                return true;
            }
        }
        return false;
    }
}

if (!function_exists('admin_config_has_primary_wood_selection')) {
    /**
     * True when the config already uses the regular storefront Wood field
     * (summary "Wood", wood_type, or wood key) — not Millwork Wood.
     */
    function admin_config_has_primary_wood_selection(array $options, array $seen = []) {
        if (isset($seen['wood'])) {
            return true;
        }
        foreach (['wood_type', 'wood'] as $k) {
            if (!isset($options[$k])) {
                continue;
            }
            $v = trim((string) ($options[$k] === null ? '' : $options[$k]));
            if ($v !== '') {
                return true;
            }
        }
        $summary = (isset($options['_selected_summary']) && is_array($options['_selected_summary']))
            ? $options['_selected_summary'] : [];
        foreach ($summary as $item) {
            if (!is_array($item)) {
                continue;
            }
            $nl = admin_norm_selected_option_label((string) ($item['label'] ?? ''));
            $value = trim((string) ($item['value'] ?? ''));
            if ($nl === 'wood' && $value !== '') {
                return true;
            }
        }
        return false;
    }
}

if (!function_exists('admin_should_show_millwork_wood_selected_option')) {
    /**
     * Millwork Wood is only shown when it was explicitly saved for that product.
     * Hide stale millwork_wood keys when the config already has regular Wood.
     */
    function admin_should_show_millwork_wood_selected_option(array $options, array $seen = []) {
        if (admin_config_summary_has_millwork_wood($options)) {
            return true;
        }
        $mw = trim((string) ($options['millwork_wood'] ?? ''));
        if ($mw === '') {
            return false;
        }
        return !admin_config_has_primary_wood_selection($options, $seen);
    }
}

if (!function_exists('admin_selected_option_core_label')) {
    /**
     * Reduce a normalized label to its significant token(s) for fuzzy matching between
     * the admin field label (e.g. "custom length", "wood") and the storefront option
     * metaobject label (e.g. "Length", "Wood Species"). Drops generic qualifier words.
     */
    function admin_selected_option_core_label($nl) {
        $nl = admin_norm_selected_option_label($nl);
        $nl = preg_replace('/\s*\(\s*fraction\s*\)\s*/i', ' ', $nl);
        $nl = preg_replace('/\s+fraction\s*$/i', '', $nl);
        $nl = preg_replace('/\s+/', ' ', trim($nl));
        $stop = ['custom', 'species', 'option', 'options', 'size', 'type', 'select', 'choose', 'your'];
        $tokens = array_values(array_filter(explode(' ', $nl), function ($t) use ($stop) {
            return $t !== '' && !in_array($t, $stop, true);
        }));
        return implode(' ', $tokens);
    }
}

if (!function_exists('admin_is_millwork_selected_option_key')) {
    /** Keys/labels that belong in Selected options (millwork), not measurement accordions. */
    function admin_is_millwork_selected_option_key($keyOrLabel) {
        $n = admin_norm_option_id($keyOrLabel);
        return in_array($n, ['millwork_wood', 'custom_length', 'custom_thickness', 'wood'], true);
    }
}

if (!function_exists('admin_labels_refer_to_same_measurement_field')) {
    function admin_labels_refer_to_same_measurement_field($labelA, $labelB) {
        $a = admin_norm_selected_option_label($labelA);
        $b = admin_norm_selected_option_label($labelB);
        if ($a === '' || $b === '') {
            return false;
        }
        if ($a === $b) {
            return true;
        }
        $strip = function ($s) {
            $s = preg_replace('/\s*\(\s*fraction\s*\)\s*/i', ' ', $s);
            $s = preg_replace('/\s+fraction\s*$/i', '', $s);
            return trim(preg_replace('/\s+/', ' ', $s));
        };
        if ($strip($a) === $strip($b)) {
            return true;
        }
        $coreA = admin_selected_option_core_label($strip($a));
        $coreB = admin_selected_option_core_label($strip($b));
        return $coreA !== '' && $coreA === $coreB;
    }
}

if (!function_exists('admin_has_populated_measurement_thickness')) {
    function admin_has_populated_measurement_thickness(array $options, array $schema = []) {
        foreach (['exact_door_thickness_frac', 'exact_door_thickness', 'door_thickness', 'interior_door_thickness', 'screen_storm_thickness_frac', 'screen_storm_thickness'] as $mid) {
            if (admin_config_get_measurement_value($options, $mid) !== null) {
                return true;
            }
        }
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id']) || !admin_is_measurement_schema_option($opt)) {
                continue;
            }
            $oid = admin_norm_option_id((string) $opt['id']);
            $label = strtolower((string) ($opt['label'] ?? ''));
            if (!preg_match('/thick/i', $oid) && strpos($label, 'thick') === false) {
                continue;
            }
            if (admin_config_get_measurement_value($options, $oid) !== null) {
                return true;
            }
        }
        return false;
    }
}

if (!function_exists('admin_is_measurement_managed_field_label')) {
    /**
     * True when this label is already editable in the Measurements accordion
     * (e.g. Exact Door Thickness vs Exact Door Thickness (fraction)).
     */
    function admin_is_measurement_managed_field_label($label, array $schema, array $options = []) {
        $nl = admin_norm_selected_option_label($label);
        if ($nl === '') {
            return false;
        }
        if (admin_is_millwork_selected_option_key($nl)) {
            return false;
        }
        if (strpos($nl, 'custom ') === 0) {
            return false;
        }

        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id']) || !admin_is_measurement_schema_option($opt)) {
                continue;
            }
            $optLabel = (string) ($opt['label'] ?? $opt['id']);
            if (admin_labels_refer_to_same_measurement_field($label, $optLabel)) {
                return true;
            }
        }

        if (preg_match('/\bthick/i', $nl) && admin_has_populated_measurement_thickness($options, $schema)) {
            return true;
        }

        return false;
    }
}

if (!function_exists('admin_measurement_display_value_for_summary')) {
    function admin_measurement_display_value_for_summary(array $opt, $rawValue) {
        $raw = trim((string) $rawValue);
        if ($raw === '') {
            return '';
        }
        $choices = (isset($opt['options']) && is_array($opt['options'])) ? $opt['options'] : [];
        foreach ($choices as $c) {
            if (!is_array($c)) {
                continue;
            }
            $cv = (string) ($c['value'] ?? '');
            $cl = (string) ($c['label'] ?? $cv);
            if (admin_select_value_matches($raw, $cv) || $cl === $raw) {
                return $cl !== '' ? $cl : $cv;
            }
        }
        return $raw;
    }
}

if (!function_exists('admin_sync_measurement_fields_in_summary')) {
    /** Keep _selected_summary in sync when measurement dropdowns change (no duplicate UI). */
    function admin_sync_measurement_fields_in_summary(array $options, array $schema) {
        if (!isset($options['_selected_summary']) || !is_array($options['_selected_summary'])) {
            $options['_selected_summary'] = [];
        }
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id']) || !admin_is_measurement_schema_option($opt)) {
                continue;
            }
            $label = trim((string) ($opt['label'] ?? $opt['id']));
            $value = admin_config_get_measurement_value($options, (string) $opt['id']);
            if ($value === null || $value === '') {
                continue;
            }
            $display = admin_measurement_display_value_for_summary($opt, $value);
            foreach ($options['_selected_summary'] as &$item) {
                if (!is_array($item)) {
                    continue;
                }
                if (!admin_labels_refer_to_same_measurement_field($label, (string) ($item['label'] ?? ''))) {
                    continue;
                }
                $item['value'] = $display;
            }
            unset($item);
            if (isset($options['_line_item_properties']) && is_array($options['_line_item_properties'])) {
                foreach ($options['_line_item_properties'] as $k => $v) {
                    if (admin_labels_refer_to_same_measurement_field($label, (string) $k)) {
                        $options['_line_item_properties'][$k] = $display;
                    }
                }
            }
        }
        return $options;
    }
}

if (!function_exists('admin_match_choices_for_label')) {
    /**
     * Find the best-matching choice list for a field label from a map keyed by
     * normalized storefront option label. Uses exact, then core-token, matching.
     * Returns [] when nothing matches.
     */
    function admin_match_choices_for_label($label, array $choiceMap) {
        if (empty($choiceMap)) return [];
        $nl = admin_norm_selected_option_label($label);
        if ($nl !== '' && isset($choiceMap[$nl]) && is_array($choiceMap[$nl]) && !empty($choiceMap[$nl])) {
            return $choiceMap[$nl];
        }
        $core = admin_selected_option_core_label($nl);
        if ($core === '') return [];
        foreach ($choiceMap as $key => $choices) {
            if (!is_array($choices) || empty($choices)) continue;
            $keyCore = admin_selected_option_core_label((string) $key);
            if ($keyCore === '') continue;
            if ($keyCore === $core
                || strpos($keyCore, $core) !== false
                || strpos($core, $keyCore) !== false) {
                return $choices;
            }
        }
        return [];
    }
}

if (!function_exists('admin_fetch_metaobject_with_refs')) {
    /** Fetch a single metaobject by GID, expanding reference fields one level (its choices). */
    function admin_fetch_metaobject_with_refs($gid) {
        $q = <<<'GQL'
query($id: ID!) {
  metaobject(id: $id) {
    id
    type
    handle
    displayName
    fields {
      key
      value
      type
      references(first: 100) {
        nodes {
          __typename
          ... on Metaobject { id type handle displayName fields { key value } }
        }
      }
    }
  }
}
GQL;
        $res = admin_shopify_gql_request($q, ['id' => (string) $gid]);
        if (empty($res['ok'])) return null;
        return $res['data']['metaobject'] ?? null;
    }
}

if (!function_exists('admin_extract_choice_display')) {
    /** Pick a human display string from a metaobject's flattened field map. */
    function admin_extract_choice_display(array $fields, $fallbackDisplayName = '') {
        foreach (['label', 'name', 'title', 'value', 'display_name', 'option_label', 'wood', 'species'] as $k) {
            if (isset($fields[$k]) && trim((string) $fields[$k]) !== '') {
                return trim((string) $fields[$k]);
            }
        }
        $fb = trim((string) $fallbackDisplayName);
        return $fb;
    }
}

if (!function_exists('admin_extract_choice_value_label')) {
    /**
     * Split a metaobject choice row into machine value + human label.
     * Storefront schema uses value for <select> values and label for display.
     */
    function admin_extract_choice_value_label(array $fields, $fallbackDisplayName = '') {
        $label = '';
        foreach (['label', 'name', 'title', 'display_name', 'option_label', 'wood', 'species'] as $k) {
            if (isset($fields[$k]) && trim((string) $fields[$k]) !== '') {
                $label = trim((string) $fields[$k]);
                break;
            }
        }
        $value = '';
        foreach (['value', 'handle', 'slug', 'key', 'option_value', 'option_id'] as $k) {
            if (isset($fields[$k]) && trim((string) $fields[$k]) !== '') {
                $value = trim((string) $fields[$k]);
                break;
            }
        }
        if ($label === '') {
            $label = trim((string) $fallbackDisplayName);
        }
        if ($value === '') {
            $value = $label;
        }
        if ($label === '') {
            $label = $value;
        }
        return ['value' => $value, 'label' => $label];
    }
}

if (!function_exists('admin_normalize_choice_list')) {
    /**
     * @param mixed $choices
     * @return list<array{value: string, label: string}>
     */
    function admin_normalize_choice_list($choices) {
        if (!is_array($choices)) {
            return [];
        }
        $out = [];
        $seen = [];
        foreach ($choices as $c) {
            if (!is_array($c)) {
                $s = trim((string) $c);
                if ($s === '') {
                    continue;
                }
                $c = ['value' => $s, 'label' => $s];
            }
            $v = trim((string) ($c['value'] ?? $c['label'] ?? ''));
            $l = trim((string) ($c['label'] ?? $c['value'] ?? ''));
            if ($v === '' && $l === '') {
                continue;
            }
            if ($v === '') {
                $v = $l;
            }
            if ($l === '') {
                $l = function_exists('humanize_token') ? humanize_token($v) : $v;
            }
            $kk = admin_norm_option_value($v) . '|' . admin_norm_option_value($l);
            if (isset($seen[$kk])) {
                continue;
            }
            $seen[$kk] = true;
            $out[] = ['value' => $v, 'label' => $l];
        }
        return $out;
    }
}

if (!function_exists('admin_find_schema_option_by_label')) {
    /** Find a saved _pricing_schema option row by label or id (normalized). */
    function admin_find_schema_option_by_label(array $schema, $label) {
        $nl = admin_norm_selected_option_label($label);
        if ($nl === '') {
            return null;
        }
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id'])) {
                continue;
            }
            $optLabel = admin_norm_selected_option_label((string) ($opt['label'] ?? $opt['id']));
            $optId = admin_norm_selected_option_label((string) $opt['id']);
            if ($optLabel === $nl || $optId === $nl) {
                return $opt;
            }
            $core = admin_selected_option_core_label($nl);
            if ($core !== '') {
                $optCore = admin_selected_option_core_label($optLabel);
                if ($optCore !== '' && ($optCore === $core || strpos($optCore, $core) !== false || strpos($core, $optCore) !== false)) {
                    return $opt;
                }
            }
        }
        return null;
    }
}

if (!function_exists('admin_resolve_choice_pair')) {
    /**
     * Match a saved raw string (machine value or display label) to a choice list.
     *
     * @return array{value: string, label: string}
     */
    function admin_resolve_choice_pair($rawValue, array $choices) {
        $raw = trim((string) $rawValue);
        if ($raw === '') {
            return ['value' => '', 'label' => ''];
        }
        foreach (admin_normalize_choice_list($choices) as $c) {
            $cv = (string) ($c['value'] ?? '');
            $cl = (string) ($c['label'] ?? $cv);
            if (admin_select_value_matches($raw, $cv) || $cl === $raw
                || admin_norm_option_value($cl) === admin_norm_option_value($raw)) {
                return ['value' => $cv, 'label' => $cl !== '' ? $cl : $cv];
            }
        }
        return ['value' => $raw, 'label' => $raw];
    }
}

if (!function_exists('admin_infer_hardware_selected_option_id')) {
    /** Map a hardware summary / line-item label to its storefront schema option id. */
    function admin_infer_hardware_selected_option_id($label) {
        $nl = admin_norm_selected_option_label($label);
        $map = [
            'finish' => 'finish',
            'hardware function' => 'function',
            'function' => 'function',
            'closer color' => 'closer_color',
            'door thickness' => 'door_thickness',
            'thickness' => 'door_thickness',
            'swing direction' => 'swing_direction',
            'door backset' => 'door_backset',
            'backset' => 'door_backset',
            'track length' => 'track_length',
            'number of connected doors' => 'connected_doors',
            'connected doors' => 'connected_doors',
            'upper track length' => 'upper_track_length',
            'max door size' => 'max_door_size',
            'carriage pack type' => 'carriage_pack_type',
            'strap hinge length' => 'strap_hinge_length',
        ];
        return $map[$nl] ?? '';
    }
}

if (!function_exists('admin_enrich_options_product_context')) {
    /**
     * Ensure options_json carries product identifiers needed to load metafield choices.
     */
    function admin_enrich_options_product_context(array $options, array $config = []) {
        foreach (['product_id', 'base_product_id'] as $k) {
            if (!empty($config[$k]) && empty($options['_product_id']) && empty($options['product_id'])) {
                $options['_product_id'] = (string) $config[$k];
                break;
            }
        }
        if (empty($options['_product_id']) && empty($options['product_id'])) {
            foreach (admin_resolve_hardware_selections($options) as $row) {
                if (!is_array($row) || empty($row['product_id'])) {
                    continue;
                }
                $pid = (string) $row['product_id'];
                if (preg_match('/(\d+)/', $pid, $m)) {
                    $options['_product_id'] = $m[1];
                } else {
                    $options['_product_id'] = $pid;
                }
                break;
            }
        }
        if (!empty($config['product_handle']) && empty($options['_product_handle'])) {
            $options['_product_handle'] = (string) $config['product_handle'];
        }
        if (!empty($config['variant_id']) && empty($options['_variant_id'])) {
            $options['_variant_id'] = (string) $config['variant_id'];
        }
        return $options;
    }
}

if (!function_exists('admin_merge_choice_lists_prefer_primary')) {
    /**
     * Merge two choice lists; primary (metafield) order wins, secondary fills gaps.
     *
     * @return list<array{value: string, label: string}>
     */
    function admin_merge_choice_lists_prefer_primary(array $primary, array $secondary) {
        $out = admin_normalize_choice_list($primary);
        $seen = [];
        foreach ($out as $c) {
            $seen[admin_norm_option_value((string) ($c['value'] ?? ''))] = true;
            $seen[admin_norm_option_value((string) ($c['label'] ?? ''))] = true;
        }
        foreach (admin_normalize_choice_list($secondary) as $c) {
            $vk = admin_norm_option_value((string) ($c['value'] ?? ''));
            $lk = admin_norm_option_value((string) ($c['label'] ?? ''));
            if ($vk !== '' && isset($seen[$vk])) {
                continue;
            }
            if ($lk !== '' && isset($seen[$lk])) {
                continue;
            }
            $out[] = $c;
            if ($vk !== '') {
                $seen[$vk] = true;
            }
            if ($lk !== '') {
                $seen[$lk] = true;
            }
        }
        return $out;
    }
}

if (!function_exists('admin_extract_option_metaobject_label')) {
    /** Human label for a door-config option metaobject row. */
    function admin_extract_option_metaobject_label(array $own, array $mo = []) {
        foreach (['label', 'option_label', 'field_label', 'name', 'title', 'option_name', 'display_name'] as $k) {
            if (isset($own[$k]) && trim((string) $own[$k]) !== '') {
                return trim((string) $own[$k]);
            }
        }
        return admin_extract_choice_display($own, (string) ($mo['displayName'] ?? $mo['type'] ?? ''));
    }
}

if (!function_exists('admin_extract_option_metaobject_id')) {
    /** Machine id/handle for a door-config option metaobject row. */
    function admin_extract_option_metaobject_id(array $own, array $mo = []) {
        foreach (['option_id', 'id', 'handle', 'key', 'field_id'] as $k) {
            if (isset($own[$k]) && trim((string) $own[$k]) !== '') {
                return trim((string) $own[$k]);
            }
        }
        $handle = trim((string) ($mo['handle'] ?? ''));
        return $handle;
    }
}

if (!function_exists('admin_storefront_choice_map_for_options')) {
    /** Cached metafield/metaobject choice map for one configuration's product. */
    function admin_storefront_choice_map_for_options(array $options) {
        static $memo = [];
        $pid = '';
        foreach (['_product_id', 'product_id', 'base_product_id'] as $k) {
            if (isset($options[$k]) && preg_match('/(\d+)/', (string) $options[$k], $m)) {
                $pid = $m[1];
                break;
            }
        }
        if ($pid === '') {
            return [];
        }
        if (!isset($memo[$pid]) && function_exists('admin_millwork_storefront_choice_map')) {
            $memo[$pid] = admin_millwork_storefront_choice_map($options);
        }
        return is_array($memo[$pid] ?? null) ? $memo[$pid] : [];
    }
}

if (!function_exists('admin_resolve_field_choices_for_label')) {
    /**
     * Resolve dropdown choices for a field label.
     * Priority: live Shopify metafield/metaobjects → saved _pricing_schema → hardware row → static catalog.
     */
    function admin_resolve_field_choices_for_label(array $options, $label, array $schemaChoices = [], $hardwareOptionsKey = '') {
        $schema = (isset($options['_pricing_schema']) && is_array($options['_pricing_schema'])) ? $options['_pricing_schema'] : [];

        $liveChoices = [];
        $liveMap = admin_storefront_choice_map_for_options($options);
        if (!empty($liveMap)) {
            $live = admin_match_choices_for_label($label, $liveMap);
            if (!empty($live)) {
                $liveChoices = admin_normalize_choice_list($live);
            }
        }

        $schemaChoicesList = admin_normalize_choice_list($schemaChoices);
        if (empty($schemaChoicesList)) {
            $schemaOpt = admin_find_schema_option_by_label($schema, $label);
            if (is_array($schemaOpt) && !empty($schemaOpt['options']) && is_array($schemaOpt['options'])) {
                $schemaChoicesList = admin_normalize_choice_list($schemaOpt['options']);
            }
        }

        if (!empty($liveChoices) && !empty($schemaChoicesList)) {
            $choices = admin_merge_choice_lists_prefer_primary($liveChoices, $schemaChoicesList);
        } elseif (!empty($liveChoices)) {
            $choices = $liveChoices;
        } else {
            $choices = $schemaChoicesList;
        }

        if (empty($choices) && $hardwareOptionsKey !== '') {
            foreach (admin_resolve_hardware_selections($options) as $row) {
                if (!is_array($row) || empty($row[$hardwareOptionsKey]) || !is_array($row[$hardwareOptionsKey])) {
                    continue;
                }
                $choices = admin_normalize_choice_list($row[$hardwareOptionsKey]);
                if (!empty($choices)) {
                    break;
                }
            }
        }

        if (empty($choices) && function_exists('admin_millwork_selected_option_choices')) {
            $catalog = admin_millwork_selected_option_choices();
            $fromCatalog = admin_match_choices_for_label($label, $catalog);
            if (!empty($fromCatalog)) {
                $choices = admin_normalize_choice_list($fromCatalog);
            }
        }

        return $choices;
    }
}

if (!function_exists('admin_options_with_hardware_product_context')) {
    /** Prefer the hardware line's product_id when loading live metafield choice lists. */
    function admin_options_with_hardware_product_context(array $options, array $item) {
        if (!empty($item['product_id'])) {
            $pid = (string) $item['product_id'];
            if (preg_match('/(\d+)/', $pid, $m)) {
                $options['_product_id'] = $m[1];
            } else {
                $options['_product_id'] = $pid;
            }
        }
        return $options;
    }
}

if (!function_exists('admin_resolve_live_choices_for_hardware_field')) {
    /**
     * Fetch live Shopify metafield choices for one hardware landing field.
     * Tries prop label, schema id, and summary label aliases.
     */
    function admin_resolve_live_choices_for_hardware_field(array $options, $propLabel, $schemaId = '', array $summaryLabels = [], array $schemaAliases = []) {
        $liveMap = admin_storefront_choice_map_for_options($options);
        if (empty($liveMap)) {
            return [];
        }
        $needles = [];
        foreach (array_merge([(string) $propLabel, (string) $schemaId], $summaryLabels, $schemaAliases) as $needle) {
            $needle = trim((string) $needle);
            if ($needle !== '') {
                $needles[] = $needle;
            }
        }
        $needles = array_values(array_unique($needles));
        foreach ($needles as $needle) {
            $live = admin_match_choices_for_label($needle, $liveMap);
            if (!empty($live)) {
                return admin_normalize_choice_list($live);
            }
        }
        return [];
    }
}

if (!function_exists('admin_hardware_merged_field_choices_for_item')) {
    /**
     * Hardware admin dropdown choices: live metafield list (primary) + saved row options + current value.
     * Keeps existing row-only fallback when Shopify API is unavailable.
     */
    function admin_hardware_merged_field_choices_for_item(array $item, array $options, array $def) {
        $propLabel = (string) ($def['prop_label'] ?? '');
        $optionsKey = (string) ($def['options_key'] ?? '');
        $valueKey = (string) ($def['value_key'] ?? '');
        $labelKey = (string) ($def['label_key'] ?? '');
        $schemaId = (string) ($def['schema_id'] ?? '');
        $summaryLabels = array_values(array_filter(array_map('strval', (array) ($def['summary_labels'] ?? []))));
        $schemaAliases = array_values(array_filter(array_map('strval', (array) ($def['schema_id_aliases'] ?? []))));

        $opts = admin_options_with_hardware_product_context($options, $item);
        $fromRow = admin_hardware_field_choices_for_item($item, $optionsKey, $valueKey, $labelKey);

        $live = admin_resolve_live_choices_for_hardware_field($opts, $propLabel, $schemaId, $summaryLabels, $schemaAliases);

        $schemaChoices = [];
        if ($propLabel !== '') {
            $schema = (isset($opts['_pricing_schema']) && is_array($opts['_pricing_schema'])) ? $opts['_pricing_schema'] : [];
            $schemaOpt = admin_find_schema_option_by_label($schema, $propLabel);
            if (is_array($schemaOpt) && !empty($schemaOpt['options']) && is_array($schemaOpt['options'])) {
                $schemaChoices = admin_normalize_choice_list($schemaOpt['options']);
            }
            if (empty($schemaChoices) && $schemaId !== '') {
                foreach ($schema as $opt) {
                    if (!is_array($opt) || empty($opt['id'])) {
                        continue;
                    }
                    if (admin_norm_option_id((string) $opt['id']) !== admin_norm_option_id($schemaId)) {
                        continue;
                    }
                    if (!empty($opt['options']) && is_array($opt['options'])) {
                        $schemaChoices = admin_normalize_choice_list($opt['options']);
                    }
                    break;
                }
            }
        }

        $merged = [];
        if (!empty($live)) {
            $merged = admin_merge_choice_lists_prefer_primary($live, $fromRow);
        } elseif (!empty($schemaChoices)) {
            $merged = admin_merge_choice_lists_prefer_primary($schemaChoices, $fromRow);
        } else {
            $merged = $fromRow;
        }

        if (empty($merged) && $propLabel !== '') {
            $merged = admin_resolve_field_choices_for_label($opts, $propLabel, $schemaChoices, $optionsKey);
        }

        return admin_ensure_current_choice_in_list(
            $merged,
            trim((string) ($item[$valueKey] ?? '')),
            trim((string) ($item[$labelKey] ?? ''))
        );
    }
}

if (!function_exists('admin_norm_hardware_choice_slug')) {
    /** Same normalization the storefront uses to compare allow-list entries. */
    function admin_norm_hardware_choice_slug($s) {
        $s = strtolower(trim((string) $s));
        $s = str_replace('-', '_', $s);
        $s = preg_replace('/\s+/', '_', $s);
        return (string) $s;
    }
}

if (!function_exists('admin_parse_hardware_allow_slugs')) {
    /**
     * Parse an allow-list metafield (JSON list or comma string) into a normalized slug set.
     *
     * @return array<string, true>
     */
    function admin_parse_hardware_allow_slugs($rawValue) {
        $raw = trim((string) $rawValue);
        if ($raw === '') {
            return [];
        }
        $decoded = json_decode($raw, true);
        $items = is_array($decoded) ? $decoded : preg_split('/\s*,\s*/', $raw);
        $slugs = [];
        foreach ((array) $items as $item) {
            if (is_array($item)) {
                $item = $item['value'] ?? $item['label'] ?? '';
            }
            $slug = admin_norm_hardware_choice_slug($item);
            if ($slug !== '') {
                $slugs[$slug] = true;
            }
        }
        return $slugs;
    }
}

if (!function_exists('admin_fetch_hardware_allow_lists')) {
    /**
     * Fetch the product's per-field allow-list metafields (mirrors the storefront filter).
     * Keyed by metafield key => normalized slug set. Returns [] when unconfigured.
     *
     * @return array<string, array<string, true>>
     */
    function admin_fetch_hardware_allow_lists($pid) {
        static $memo = [];
        $pid = (string) $pid;
        if ($pid === '') {
            return [];
        }
        if (isset($memo[$pid])) {
            return $memo[$pid];
        }
        $result = [];
        if (!function_exists('admin_shopify_is_configured') || !admin_shopify_is_configured()) {
            $memo[$pid] = $result;
            return $result;
        }
        $keys = [];
        foreach (admin_hardware_landing_field_defs() as $def) {
            if (!is_array($def)) {
                continue;
            }
            foreach ((array) ($def['allow_metafields'] ?? []) as $mk) {
                $mk = trim((string) $mk);
                if ($mk !== '') {
                    $keys[$mk] = true;
                }
            }
        }
        if (empty($keys)) {
            $memo[$pid] = $result;
            return $result;
        }
        $fields = [];
        $aliasToKey = [];
        $i = 0;
        foreach (array_keys($keys) as $mk) {
            $alias = 'mf' . $i++;
            $aliasToKey[$alias] = $mk;
            $fields[] = $alias . ': metafield(namespace: "custom", key: ' . json_encode($mk) . ') { value type }';
        }
        $query = "query(\$id: ID!) {\n  product(id: \$id) {\n    " . implode("\n    ", $fields) . "\n  }\n}";
        $res = admin_shopify_gql_request($query, ['id' => 'gid://shopify/Product/' . $pid]);
        if (!empty($res['ok'])) {
            $product = $res['data']['product'] ?? [];
            foreach ($aliasToKey as $alias => $mk) {
                $mf = $product[$alias] ?? null;
                if (!is_array($mf)) {
                    continue;
                }
                $slugs = admin_parse_hardware_allow_slugs($mf['value'] ?? '');
                if (!empty($slugs)) {
                    $result[$mk] = $slugs;
                }
            }
        }
        $memo[$pid] = $result;
        return $result;
    }
}

if (!function_exists('admin_apply_hardware_allow_list_filter')) {
    /**
     * Keep only choices whose value/label slug is in the field's allow list, matching the
     * storefront. Returns the original list when no allow list is configured, or as a safety
     * fallback when nothing matches (so the admin dropdown is never emptied unexpectedly).
     */
    function admin_apply_hardware_allow_list_filter(array $choices, array $def, array $allowLists) {
        foreach ((array) ($def['allow_metafields'] ?? []) as $mk) {
            $mk = trim((string) $mk);
            if ($mk === '' || empty($allowLists[$mk])) {
                continue;
            }
            $allow = $allowLists[$mk];
            $filtered = [];
            foreach ($choices as $c) {
                if (!is_array($c)) {
                    continue;
                }
                $v = admin_norm_hardware_choice_slug($c['value'] ?? '');
                $l = admin_norm_hardware_choice_slug($c['label'] ?? '');
                if (($v !== '' && isset($allow[$v])) || ($l !== '' && isset($allow[$l]))) {
                    $filtered[] = $c;
                }
            }
            return !empty($filtered) ? $filtered : $choices;
        }
        return $choices;
    }
}

if (!function_exists('admin_hardware_field_available_for_product')) {
    /**
     * True when a hardware field actually exists on the storefront product, i.e. it is present
     * in the live custom.door_config_options choice map (or its thickness/backset list
     * metafield). Returns true when the live map is unavailable (Shopify API unconfigured or
     * no product id) so the admin keeps its existing behavior and never hides real fields.
     */
    function admin_hardware_field_available_for_product(array $def, array $options) {
        $liveMap = admin_storefront_choice_map_for_options($options);
        if (empty($liveMap)) {
            return true;
        }
        foreach (admin_hardware_field_schema_needles($def) as $needle) {
            if (!empty(admin_match_choices_for_label($needle, $liveMap))) {
                return true;
            }
        }
        return false;
    }
}

if (!function_exists('admin_strip_unavailable_hardware_fields')) {
    /**
     * Remove saved values for ALL hardware fields that are not available on the storefront
     * product (not present in door_config_options / list metafields). No-op when the live map
     * is unavailable, so nothing is lost when the Shopify API is offline.
     */
    function admin_strip_unavailable_hardware_fields(array $item, array $options) {
        $opts = admin_options_with_hardware_product_context($options, $item);
        $liveMap = admin_storefront_choice_map_for_options($opts);
        if (empty($liveMap)) {
            return $item;
        }
        foreach (admin_hardware_landing_field_defs() as $def) {
            if (!is_array($def)) {
                continue;
            }
            $vk = (string) ($def['value_key'] ?? '');
            if ($vk === '') {
                continue;
            }
            if (admin_hardware_field_available_for_product($def, $opts)) {
                continue;
            }
            $lk = (string) ($def['label_key'] ?? '');
            $ok = (string) ($def['options_key'] ?? '');
            unset($item[$vk]);
            if ($lk !== '') {
                unset($item[$lk]);
            }
            if ($ok !== '') {
                unset($item[$ok]);
            }
        }
        return $item;
    }
}

if (!function_exists('admin_millwork_storefront_choice_map')) {
    /**
     * Option A: fetch the storefront option choice lists for a product live from Shopify,
     * via the product's custom.door_config_options metaobject references. Returns a map
     * keyed by NORMALIZED option label => list of ['value' => ..., 'label' => ...], so the
     * admin "Selected options" dropdowns always mirror the storefront for ANY millwork
     * product. Falls back silently (returns []) when the API is unconfigured, the product
     * has no config options, or anything is unrecognized. Cached per product id (process +
     * short-lived file cache) to avoid repeat calls on every admin page load.
     */
    function admin_millwork_storefront_choice_map(array $options) {
        static $memo = [];

        $pid = '';
        foreach (['_product_id', 'product_id', 'base_product_id'] as $k) {
            if (isset($options[$k]) && preg_match('/(\d+)/', (string) $options[$k], $m)) {
                $pid = $m[1];
                break;
            }
        }
        if ($pid === '') return [];
        if (array_key_exists($pid, $memo)) return $memo[$pid];
        $cacheFile = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'mw_optchoices_' . $pid . '.json';
        $ttl = 21600; // 6 hours
        if (is_file($cacheFile) && (time() - @filemtime($cacheFile)) < $ttl) {
            $cached = json_decode((string) @file_get_contents($cacheFile), true);
            if (is_array($cached)) {
                $memo[$pid] = $cached;
                return $cached;
            }
        }

        $map = [];
        if (function_exists('admin_shopify_is_configured') && admin_shopify_is_configured()) {
            $allowLists = admin_fetch_hardware_allow_lists($pid);
            $prodQuery = <<<'GQL'
query($id: ID!) {
  product(id: $id) {
    metafield(namespace: "custom", key: "door_config_options") { value type }
  }
}
GQL;
            $pgid = 'gid://shopify/Product/' . $pid;
            $pres = admin_shopify_gql_request($prodQuery, ['id' => $pgid]);
            $gids = [];
            if (!empty($pres['ok'])) {
                $raw = (string) ($pres['data']['product']['metafield']['value'] ?? '');
                $decoded = json_decode($raw, true);
                if (is_array($decoded)) {
                    foreach ($decoded as $g) {
                        if (is_string($g) && $g !== '') $gids[] = $g;
                    }
                }
            }

            foreach ($gids as $gid) {
                $mo = admin_fetch_metaobject_with_refs($gid);
                if (!is_array($mo)) continue;

                // Flatten this option's own fields, and find label + choices.
                $own = [];
                $choices = [];
                foreach (($mo['fields'] ?? []) as $f) {
                    $key = (string) ($f['key'] ?? '');
                    if ($key !== '') {
                        $own[$key] = $f['value'];
                    }

                    $refNodes = $f['references']['nodes'] ?? [];
                    foreach ($refNodes as $rn) {
                        $rf = [];
                        foreach (($rn['fields'] ?? []) as $rfld) {
                            $rf[(string) ($rfld['key'] ?? '')] = $rfld['value'];
                        }
                        $pair = admin_extract_choice_value_label($rf, (string) ($rn['displayName'] ?? ''));
                        if (($pair['value'] ?? '') !== '' || ($pair['label'] ?? '') !== '') {
                            $choices[] = $pair;
                        }
                    }
                }

                // If no reference-based choices, look for a list-type field holding values.
                if (empty($choices)) {
                    foreach (($mo['fields'] ?? []) as $f) {
                        $ftype = (string) ($f['type'] ?? '');
                        if (strpos($ftype, 'list.') !== 0) continue;
                        $arr = json_decode((string) ($f['value'] ?? ''), true);
                        if (is_array($arr) && !empty($arr)) {
                            foreach ($arr as $v) {
                                if (is_scalar($v)) {
                                    $s = trim((string) $v);
                                    if ($s !== '') $choices[] = ['value' => $s, 'label' => $s];
                                }
                            }
                            if (!empty($choices)) break;
                        }
                    }
                }

                if (empty($choices)) continue;

                $label = admin_extract_option_metaobject_label($own, $mo);
                $optionId = admin_extract_option_metaobject_id($own, $mo);
                $nl = admin_norm_selected_option_label($label);
                if ($nl === '') continue;

                // De-dupe choices preserving order.
                $uniq = [];
                $seenC = [];
                foreach ($choices as $c) {
                    $cv = (string) ($c['value'] ?? '');
                    $cl = (string) ($c['label'] ?? $cv);
                    $kk = admin_norm_option_value($cv) . '|' . admin_norm_option_value($cl);
                    if ($cv === '' && $cl === '') continue;
                    if (isset($seenC[$kk])) continue;
                    $seenC[$kk] = true;
                    $uniq[] = ['value' => $cv !== '' ? $cv : $cl, 'label' => $cl !== '' ? $cl : $cv];
                }
                if (!empty($uniq)) {
                    $matchedDef = admin_find_hardware_field_def_for_metaobject_key($optionId, $label);
                    if (is_array($matchedDef)) {
                        // Show only the values the storefront allows (allow-list metafield).
                        $uniq = admin_apply_hardware_allow_list_filter($uniq, $matchedDef, $allowLists);
                    }
                    $registerNeedles = $matchedDef
                        ? admin_hardware_field_schema_needles($matchedDef)
                        : array_values(array_filter([$label, $optionId]));
                    admin_register_storefront_choice_map_entries($map, $registerNeedles, $uniq);
                }
            }
        }

        admin_append_hardware_product_list_metafield_choices($map, $pid);

        @file_put_contents($cacheFile, json_encode($map, JSON_UNESCAPED_UNICODE));
        $memo[$pid] = $map;
        return $map;
    }
}

if (!function_exists('admin_resolve_selected_option_fields')) {
    /**
     * Resolve customer "Selected options" that are NOT already editable on the admin
     * edit page (e.g. millwork Length / Custom Thickness / Wood Species). These surface
     * in the saved summary but have no editable control, unlike casing/hardware.
     *
     * Sources, in order:
     *   1) _pricing_schema options whose type the main loop does not render (typeless /
     *      custom), but which carry a saved value (value lives in $options[id]).
     *   2) _selected_summary entries that have no matching schema option (value-only).
     *
     * Choice lists are sourced live from Shopify (Option A) via
     * admin_millwork_storefront_choice_map(), falling back to the editable static catalog.
     *
     * Each row: source ('schema'|'summary'), id, label, value, choices[], image.
     */
    function admin_resolve_selected_option_fields(array $options) {
        $rows = [];
        $seen = [];
        $schema = (isset($options['_pricing_schema']) && is_array($options['_pricing_schema'])) ? $options['_pricing_schema'] : [];
        $catalog = function_exists('admin_millwork_selected_option_choices') ? admin_millwork_selected_option_choices() : [];
        $liveMap = function_exists('admin_millwork_storefront_choice_map') ? admin_millwork_storefront_choice_map($options) : [];

        // Resolve a choice list for a label: saved schema, live metaobjects, static catalog.
        $resolveChoices = function ($label, $schemaChoices = [], $hardwareOptionsKey = '') use ($options, $liveMap, $catalog) {
            if (function_exists('admin_is_millwork_length_field') && admin_is_millwork_length_field('', $label)) {
                $lengthChoices = admin_resolve_millwork_length_choices($label, $options, $liveMap, $catalog);
                if (!empty($lengthChoices)) {
                    return $lengthChoices;
                }
            }
            return admin_resolve_field_choices_for_label($options, $label, $schemaChoices, $hardwareOptionsKey);
        };

        $reserved = ['sidelight_design', 'glass_bevel', 'hardware_options', 'casing_options'];

        // 1) Typeless / custom schema options with a saved value.
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id'])) continue;
            $oid = (string) $opt['id'];
            $type = strtolower((string) ($opt['type'] ?? $opt['input_type'] ?? ''));
            if (admin_selected_option_field_is_handled_type($type)) continue;
            if (in_array(strtolower(str_replace('-', '_', $oid)), $reserved, true)) continue;
            if (admin_is_measurement_schema_option($opt)) continue;
            if (function_exists('admin_is_hidden_admin_fraction_field') && admin_is_hidden_admin_fraction_field($opt)) continue;
            if (admin_is_hardware_landing_schema_id($oid)) continue;

            $label = trim((string) ($opt['label'] ?? $oid));
            if ($label === '') $label = $oid;
            $value = admin_config_get_value($options, $oid);
            if (is_array($value)) {
                $value = implode(', ', array_map('strval', $value));
            }
            $value = trim((string) ($value === null ? '' : $value));
            if ($value === '') continue;

            $nl = admin_norm_selected_option_label($label);
            if ($nl === '' || isset($seen[$nl])) continue;
            $seen[$nl] = true;

            $schemaChoices = (!empty($opt['options']) && is_array($opt['options'])) ? array_values($opt['options']) : [];
            $fieldChoices = $resolveChoices($label, $schemaChoices);
            $pair = admin_resolve_choice_pair($value, $fieldChoices);
            $displayValue = ($pair['label'] ?? '') !== '' ? (string) $pair['label'] : $value;
            $storageValue = ($pair['value'] ?? '') !== '' ? (string) $pair['value'] : $value;
            $rows[] = [
                'source'  => 'schema',
                'id'      => $oid,
                'label'   => $label,
                'value'   => $displayValue,
                'storage_value' => $storageValue,
                'choices' => $fieldChoices,
                'image'   => '',
            ];
        }

        // Pre-index labels of schema options that ARE handled elsewhere, so we don't
        // duplicate them from the summary list.
        $handledLabels = [];
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id'])) continue;
            $type = strtolower((string) ($opt['type'] ?? $opt['input_type'] ?? ''));
            $isHandled = admin_selected_option_field_is_handled_type($type) || admin_is_measurement_schema_option($opt);
            if (!$isHandled) continue;
            $hl = admin_norm_selected_option_label((string) ($opt['label'] ?? $opt['id']));
            if ($hl !== '') {
                $handledLabels[$hl] = true;
                $core = admin_selected_option_core_label($hl);
                if ($core !== '') {
                    $handledLabels[$core] = true;
                }
            }
        }

        // 2) _selected_summary entries with no matching schema option.
        $seenValues = [];
        $summary = (isset($options['_selected_summary']) && is_array($options['_selected_summary'])) ? $options['_selected_summary'] : [];
        foreach ($summary as $item) {
            if (!is_array($item)) continue;
            $label = trim((string) ($item['label'] ?? ''));
            $value = trim((string) ($item['value'] ?? ''));
            if ($label === '' || $value === '') continue;

            $nl = admin_norm_selected_option_label($label);
            if ($nl === '' || isset($seen[$nl]) || isset($handledLabels[$nl])) continue;
            if (admin_is_measurement_managed_field_label($label, $schema, $options)) continue;
            if ($nl === 'millwork wood' && !admin_should_show_millwork_wood_selected_option($options, $seen)) continue;
            if ($nl === 'quantity' || $nl === 'bevel glass') continue;
            if (preg_match('/^(hardware|casing)\b/i', $label)) continue;
            if (admin_is_hardware_landing_field_label($label)) continue;
            $seen[$nl] = true;
            $seenValues[admin_norm_option_value($value)] = true;

            $inferredId = admin_infer_millwork_selected_option_id($label);
            if ($inferredId === 'custom_thickness') {
                $value = admin_millwork_thickness_display_label($value);
            }
            $hwOptionsKey = '';
            if ($inferredId !== '' && function_exists('admin_hardware_landing_field_defs')) {
                foreach (admin_hardware_landing_field_defs() as $hwDef) {
                    if (!is_array($hwDef)) {
                        continue;
                    }
                    if ((string) ($hwDef['schema_id'] ?? '') === $inferredId) {
                        $hwOptionsKey = (string) ($hwDef['options_key'] ?? '');
                        break;
                    }
                }
            }
            $fieldChoices = $resolveChoices($label, [], $hwOptionsKey);
            $pair = admin_resolve_choice_pair($value, $fieldChoices);
            $displayValue = ($pair['label'] ?? '') !== '' ? (string) $pair['label'] : $value;
            $storageValue = ($pair['value'] ?? '') !== '' ? (string) $pair['value'] : $value;
            $rows[] = [
                'source'  => 'summary',
                'id'      => $inferredId,
                'label'   => $label,
                'value'   => $displayValue,
                'storage_value' => $storageValue,
                'choices' => $fieldChoices,
                'image'   => trim((string) ($item['image'] ?? '')),
            ];
        }

        // Track which value strings are already shown (used to de-dupe the top-level
        // fallback against summary rows whose label is named differently, e.g. the
        // "Wood" summary entry vs the millwork_wood top-level key).
        foreach ($rows as $r) {
            $seenValues[admin_norm_option_value((string) ($r['value'] ?? ''))] = true;
        }

        // 3) Fallback: top-level (non-internal) option keys that hold a value but were
        //    not captured above and are not schema-driven. Covers products saved with
        //    raw keys only (e.g. millwork_wood / custom_length / custom_thickness) and
        //    no _selected_summary. De-duped by both normalized label and value so a
        //    config that has BOTH (like #112) never renders the same field twice.
        $schemaIds = [];
        foreach ($schema as $opt) {
            if (is_array($opt) && !empty($opt['id'])) {
                $schemaIds[admin_norm_option_id((string) $opt['id'])] = true;
            }
        }
        foreach ($options as $key => $val) {
            if (!is_string($key) || $key === '' || $key[0] === '_') continue;
            if (in_array(strtolower(str_replace('-', '_', $key)), $reserved, true)) continue;
            if (admin_is_hardware_landing_schema_id($key)) continue;
            if (isset($schemaIds[admin_norm_option_id($key)])) continue;
            if (function_exists('admin_is_measurement_option_id') && admin_is_measurement_option_id($key) && !admin_is_millwork_selected_option_key($key)) {
                continue;
            }
            if (is_array($val)) continue;
            $value = trim((string) ($val === null ? '' : $val));
            if ($value === '') continue;

            if (admin_norm_option_id($key) === 'millwork_wood'
                && !admin_should_show_millwork_wood_selected_option($options, $seen)) {
                continue;
            }

            $label = function_exists('humanize_token') ? (string) humanize_token($key) : $key;
            if (admin_is_measurement_managed_field_label($label, $schema, $options)) continue;
            if ($key === 'custom_thickness' && admin_has_populated_measurement_thickness($options, $schema)) continue;
            $nl = admin_norm_selected_option_label($label);
            if ($nl === '' || isset($seen[$nl]) || isset($handledLabels[$nl])) continue;
            if ($nl === 'quantity' || $nl === 'bevel glass') continue;
            if (isset($seenValues[admin_norm_option_value($value)])) continue;
            $seen[$nl] = true;
            $seenValues[admin_norm_option_value($value)] = true;

            if (admin_norm_option_id($key) === 'custom_thickness') {
                $value = admin_millwork_thickness_display_label($value);
            }
            $rows[] = [
                'source'  => 'schema',
                'id'      => $key,
                'label'   => $label,
                'value'   => $value,
                'choices' => $resolveChoices($label),
                'image'   => '',
            ];
        }

        // 4) Canonical millwork fields (Wood / Custom or Millwork Length / Custom Thickness)
        //    from any storage location — top-level keys, line-item properties, or summary.
        if (function_exists('admin_resolve_millwork_summary_field_rows')) {
            foreach (admin_resolve_millwork_summary_field_rows($options) as $row) {
                if (!is_array($row)) {
                    continue;
                }
                $label = trim((string) ($row['label'] ?? ''));
                $value = trim((string) ($row['value'] ?? ''));
                if ($label === '' || $value === '') {
                    continue;
                }
                $nl = admin_norm_selected_option_label($label);
                if ($nl === '' || isset($seen[$nl]) || isset($handledLabels[$nl])) {
                    continue;
                }
                $seen[$nl] = true;
                $seenValues[admin_norm_option_value($value)] = true;
                $rows[] = $row;
            }
        }

        $rows = admin_merge_hardware_landing_selected_option_rows($options, $rows);

        foreach ($rows as &$row) {
            if (!is_array($row)) {
                continue;
            }
            $row['choices'] = admin_ensure_current_choice_in_list(
                isset($row['choices']) && is_array($row['choices']) ? $row['choices'] : [],
                (string) ($row['storage_value'] ?? ''),
                (string) ($row['value'] ?? '')
            );
        }
        unset($row);

        $rows = admin_filter_hardware_from_selected_option_fields($rows, $options);
        foreach ($rows as &$row) {
            if (is_array($row)) {
                $row = admin_enrich_selected_option_field_values($row);
            }
        }
        unset($row);

        return $rows;
    }
}

if (!function_exists('admin_millwork_length_summary_label')) {
    /**
     * Pick "Custom Length" vs "Millwork Length" for a stored length value.
     */
    function admin_millwork_length_summary_label(array $options, $value) {
        $value = trim((string) $value);
        if ($value === '') {
            return 'Custom Length';
        }
        if (preg_match("/'|(?:\bft\b)/i", $value)) {
            return 'Millwork Length';
        }
        $baluster = function_exists('admin_millwork_default_length_choice_labels')
            ? admin_millwork_default_length_choice_labels()
            : ["7'", "8'", "10'", "12'"];
        foreach ($baluster as $bl) {
            if (admin_norm_option_value($value) === admin_norm_option_value($bl)) {
                return 'Millwork Length';
            }
        }
        if (!empty($options['length_options']) && is_array($options['length_options'])) {
            foreach ($options['length_options'] as $lo) {
                if (is_scalar($lo) && admin_norm_option_value($lo) === admin_norm_option_value($value)) {
                    return 'Millwork Length';
                }
                if (!is_array($lo)) {
                    continue;
                }
                $lv = trim((string) ($lo['value'] ?? $lo['label'] ?? ''));
                $ll = trim((string) ($lo['label'] ?? $lo['value'] ?? ''));
                if ($lv !== '' && admin_norm_option_value($value) === admin_norm_option_value($lv)) {
                    return 'Millwork Length';
                }
                if ($ll !== '' && admin_norm_option_value($value) === admin_norm_option_value($ll)) {
                    return 'Millwork Length';
                }
            }
        }
        $summary = (isset($options['_selected_summary']) && is_array($options['_selected_summary']))
            ? $options['_selected_summary'] : [];
        foreach ($summary as $item) {
            if (!is_array($item)) {
                continue;
            }
            $nl = admin_norm_selected_option_label((string) ($item['label'] ?? ''));
            if ($nl === 'millwork length') {
                return 'Millwork Length';
            }
        }
        return 'Custom Length';
    }
}

if (!function_exists('admin_pick_stored_millwork_value')) {
    /**
     * Read the first non-empty millwork value from top-level keys, summary, or line props.
     *
     * @param list<string> $keys       Top-level option keys to try (in order).
     * @param list<string> $labelHints Normalized summary / line-property labels to try.
     */
    function admin_pick_stored_millwork_value(array $options, array $keys, array $labelHints = []) {
        foreach ($keys as $key) {
            $val = admin_config_get_value($options, $key);
            if ($val === null || is_array($val)) {
                continue;
            }
            $value = trim((string) $val);
            if ($value !== '') {
                return $value;
            }
        }
        $summary = (isset($options['_selected_summary']) && is_array($options['_selected_summary']))
            ? $options['_selected_summary'] : [];
        foreach ($summary as $item) {
            if (!is_array($item)) {
                continue;
            }
            $nl = admin_norm_selected_option_label((string) ($item['label'] ?? ''));
            if ($nl === '' || !in_array($nl, $labelHints, true)) {
                continue;
            }
            $value = trim((string) ($item['value'] ?? ''));
            if ($value !== '') {
                return $value;
            }
        }
        if (isset($options['_line_item_properties']) && is_array($options['_line_item_properties'])) {
            foreach ($options['_line_item_properties'] as $k => $v) {
                $nl = admin_norm_selected_option_label((string) $k);
                if ($nl === '' || !in_array($nl, $labelHints, true)) {
                    continue;
                }
                $value = trim((string) ($v === null ? '' : $v));
                if ($value !== '') {
                    return $value;
                }
            }
        }
        foreach ($options as $key => $val) {
            if (!is_string($key) || $key === '' || $key[0] === '_') {
                continue;
            }
            if (is_array($val)) {
                continue;
            }
            $nl = admin_norm_selected_option_label($key);
            if ($nl === '' || !in_array($nl, $labelHints, true)) {
                continue;
            }
            $value = trim((string) ($val === null ? '' : $val));
            if ($value !== '') {
                return $value;
            }
        }
        $fuzzyNeedles = [];
        foreach ($labelHints as $hint) {
            if (strpos($hint, 'wood') !== false) {
                $fuzzyNeedles[] = 'wood';
            }
            if (strpos($hint, 'length') !== false) {
                $fuzzyNeedles[] = 'length';
            }
            if (strpos($hint, 'thickness') !== false) {
                $fuzzyNeedles[] = 'thickness';
            }
        }
        $fuzzyNeedles = array_values(array_unique($fuzzyNeedles));
        if (!empty($fuzzyNeedles) && isset($options['_line_item_properties']) && is_array($options['_line_item_properties'])) {
            foreach ($options['_line_item_properties'] as $k => $v) {
                $nl = admin_norm_selected_option_label((string) $k);
                if ($nl === '') {
                    continue;
                }
                foreach ($fuzzyNeedles as $needle) {
                    if (strpos($nl, $needle) === false) {
                        continue;
                    }
                    $value = trim((string) ($v === null ? '' : $v));
                    if ($value !== '') {
                        return $value;
                    }
                }
            }
        }
        return '';
    }
}

if (!function_exists('admin_schema_has_handled_wood_type')) {
    /** True when wood_type is already rendered by the main schema editor loop. */
    function admin_schema_has_handled_wood_type(array $schema) {
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id'])) {
                continue;
            }
            if (admin_norm_option_id((string) $opt['id']) !== 'wood_type') {
                continue;
            }
            $type = strtolower((string) ($opt['type'] ?? $opt['input_type'] ?? ''));
            if (admin_selected_option_field_is_handled_type($type)) {
                return true;
            }
        }
        return false;
    }
}

if (!function_exists('admin_resolve_millwork_summary_field_rows')) {
    /**
     * Resolve Wood, Custom/Millwork Length, and Custom Thickness for admin summary
     * and Selected-options UI from any persisted storage location.
     *
     * @return list<array{source: string, id: string, label: string, value: string, choices: array, image: string}>
     */
    function admin_resolve_millwork_summary_field_rows(array $options) {
        $rows = [];
        $catalog = function_exists('admin_millwork_selected_option_choices') ? admin_millwork_selected_option_choices() : [];
        $liveMap = function_exists('admin_millwork_storefront_choice_map') ? admin_millwork_storefront_choice_map($options) : [];
        $schema = (isset($options['_pricing_schema']) && is_array($options['_pricing_schema'])) ? $options['_pricing_schema'] : [];

        $resolveChoices = function ($label, $schemaChoices = []) use ($liveMap, $catalog, $options) {
            if (!empty($schemaChoices) && is_array($schemaChoices)) {
                return array_values($schemaChoices);
            }
            if (function_exists('admin_is_millwork_length_field') && admin_is_millwork_length_field('', $label)) {
                $lengthChoices = admin_resolve_millwork_length_choices($label, $options, $liveMap, $catalog);
                if (!empty($lengthChoices)) {
                    return $lengthChoices;
                }
            }
            $live = admin_match_choices_for_label($label, $liveMap);
            if (!empty($live)) {
                return $live;
            }
            $fromCatalog = admin_match_choices_for_label($label, $catalog);
            if (!empty($fromCatalog)) {
                return $fromCatalog;
            }
            $nl = admin_norm_selected_option_label($label);
            return (isset($catalog[$nl]) && is_array($catalog[$nl])) ? $catalog[$nl] : [];
        };

        $woodValue = admin_pick_stored_millwork_value(
            $options,
            ['millwork_wood', 'wood_type', 'wood', 'Wood'],
            ['wood', 'millwork wood', 'wood species', 'wood type', 'select wood']
        );
        if ($woodValue !== '') {
            $rows[] = [
                'source'  => 'schema',
                'id'      => 'millwork_wood',
                'label'   => 'Wood',
                'value'   => $woodValue,
                'choices' => $resolveChoices('Wood'),
                'image'   => '',
            ];
        }

        $lengthValue = admin_pick_stored_millwork_value(
            $options,
            ['custom_length', 'millwork_length', 'length', 'Length', 'Custom Length', 'Millwork Length'],
            ['custom length', 'millwork length', 'length']
        );
        if ($lengthValue !== '') {
            $lengthLabel = admin_millwork_length_summary_label($options, $lengthValue);
            $rows[] = [
                'source'  => 'schema',
                'id'      => 'custom_length',
                'label'   => $lengthLabel,
                'value'   => $lengthValue,
                'choices' => $resolveChoices($lengthLabel),
                'image'   => '',
            ];
        }

        $thickValue = admin_pick_stored_millwork_value(
            $options,
            ['custom_thickness', 'thickness'],
            ['custom thickness', 'thickness']
        );
        if ($thickValue !== '' && !admin_has_populated_measurement_thickness($options, $schema)) {
            $rows[] = [
                'source'  => 'schema',
                'id'      => 'custom_thickness',
                'label'   => 'Custom Thickness',
                'value'   => admin_millwork_thickness_display_label($thickValue),
                'choices' => $resolveChoices('Custom Thickness'),
                'image'   => '',
            ];
        }

        return $rows;
    }
}

if (!function_exists('admin_apply_selected_option_fields_post')) {
    /**
     * Persist admin edits to the "Selected options" fields back into the saved options.
     * - schema rows  -> write to $options[id] (+ normalized variants) so the summary,
     *                   line-item properties and quote description all pick up the change.
     * - all rows     -> mirror the new value into _selected_summary and _line_item_properties
     *                   (matched by normalized label) so value-only fields update too.
     */
    function admin_apply_selected_option_fields_post(array $options, array $posted) {
        if (empty($posted)) return $options;
        $schema = (isset($options['_pricing_schema']) && is_array($options['_pricing_schema'])) ? $options['_pricing_schema'] : [];
        foreach ($posted as $row) {
            if (!is_array($row)) continue;
            $source = (string) ($row['source'] ?? '');
            $label  = trim((string) ($row['label'] ?? ''));
            $id     = trim((string) ($row['id'] ?? ''));
            $value  = (string) ($row['value'] ?? '');
            $storageValue = trim((string) ($row['storage_value'] ?? ''));
            if ($label === '' && $id === '') continue;

            $hwOptionsKey = '';
            if ($id !== '' && function_exists('admin_hardware_landing_field_defs')) {
                foreach (admin_hardware_landing_field_defs() as $hwDef) {
                    if (!is_array($hwDef)) {
                        continue;
                    }
                    if ((string) ($hwDef['schema_id'] ?? '') === $id) {
                        $hwOptionsKey = (string) ($hwDef['options_key'] ?? '');
                        break;
                    }
                }
            }
            $choices = admin_resolve_field_choices_for_label($options, $label !== '' ? $label : $id, [], $hwOptionsKey);
            if ($storageValue !== '') {
                $pair = admin_resolve_choice_pair($storageValue, $choices);
                if (($pair['value'] ?? '') !== '') {
                    $value = (string) $pair['label'];
                }
            } elseif ($value !== '' && !empty($choices)) {
                $pair = admin_resolve_choice_pair($value, $choices);
                if (($pair['label'] ?? '') !== '') {
                    $value = (string) $pair['label'];
                }
                if ($storageValue === '' && ($pair['value'] ?? '') !== '') {
                    $storageValue = (string) $pair['value'];
                }
            }

            if (admin_is_measurement_managed_field_label($label, $schema, $options)) {
                continue;
            }
            if ($id !== '' && function_exists('admin_is_measurement_option_id') && admin_is_measurement_option_id($id) && !admin_is_millwork_selected_option_key($id)) {
                continue;
            }

            $persistStorage = $storageValue !== '' ? $storageValue : $value;
            $storageValue = $persistStorage;
            $displayValue = $value;
            if (admin_is_custom_thickness_field($id, $label) && $value !== '') {
                if ($id === '') {
                    $id = 'custom_thickness';
                }
                $storageValue = admin_normalize_millwork_thickness_storage_value($value);
                $displayValue = admin_millwork_thickness_display_label($storageValue);
                $options['custom_thickness'] = $storageValue;
            }
            if (admin_is_millwork_length_field($id, $label) && $value !== '') {
                if ($id === '') {
                    $id = 'custom_length';
                }
                $storageValue = trim($value);
                $displayValue = $storageValue;
                $options['custom_length'] = $storageValue;
            }

            if ($id !== '') {
                $persistValue = $storageValue !== '' ? $storageValue : $value;
                if (admin_norm_option_id($id) === 'custom_thickness' && $storageValue !== '') {
                    $persistValue = admin_normalize_millwork_thickness_storage_value($storageValue);
                }
                $options[$id] = $persistValue === '' ? null : $persistValue;
                $norm = admin_norm_option_id($id);
                if ($norm !== $id) {
                    $options[$norm] = $options[$id];
                }
                $hyphen = str_replace('_', '-', $norm);
                if ($hyphen !== $id && $hyphen !== $norm) {
                    $options[$hyphen] = $options[$id];
                }
                if ($norm === 'millwork_wood' && $value !== '') {
                    $options['wood_type'] = $value;
                }

                if (admin_is_hardware_landing_schema_id($id)) {
                    foreach (admin_hardware_landing_field_defs() as $hwDef) {
                        if (!is_array($hwDef)) {
                            continue;
                        }
                        if (admin_norm_option_id((string) ($hwDef['schema_id'] ?? '')) !== $norm) {
                            continue;
                        }
                        $hwRows = admin_resolve_hardware_selections($options);
                        if (empty($hwRows)) {
                            $hwRows = [[]];
                        }
                        $hwRow = is_array($hwRows[0]) ? $hwRows[0] : [];
                        $vk = (string) ($hwDef['value_key'] ?? '');
                        $lk = (string) ($hwDef['label_key'] ?? '');
                        $machine = $storageValue !== '' ? $storageValue : $persistValue;
                        $display = $displayValue !== '' ? $displayValue : $value;
                        if ($vk !== '' && $machine !== '') {
                            $hwRow[$vk] = $machine;
                        }
                        if ($lk !== '' && $display !== '') {
                            $hwRow[$lk] = $display;
                        }
                        $hwRows[0] = $hwRow;
                        $hwRows = admin_filter_hardware_selections(admin_normalize_hardware_selections($hwRows));
                        if (!empty($hwRows)) {
                            $options['_hardware_selections'] = $hwRows;
                            $options['hardware_options'] = $hwRows;
                        }
                        break;
                    }
                }
            }

            $nl = admin_norm_selected_option_label($label);
            if ($nl === '') continue;

            if (admin_is_custom_thickness_field($id, $label) && $displayValue !== '') {
                $value = $displayValue;
            }

            // Capture the previous displayed value (before overwrite) so we can safely
            // sync raw storefront keys (e.g. custom_length / millwork_wood) only when
            // their stored value matches the displayed one.
            $oldValue = null;
            if (isset($options['_selected_summary']) && is_array($options['_selected_summary'])) {
                foreach ($options['_selected_summary'] as $itemPeek) {
                    if (is_array($itemPeek) && admin_norm_selected_option_label((string) ($itemPeek['label'] ?? '')) === $nl) {
                        $oldValue = (string) ($itemPeek['value'] ?? '');
                        break;
                    }
                }
            }

            if (isset($options['_selected_summary']) && is_array($options['_selected_summary'])) {
                foreach ($options['_selected_summary'] as &$item) {
                    if (!is_array($item)) continue;
                    if (admin_norm_selected_option_label((string) ($item['label'] ?? '')) === $nl) {
                        $item['value'] = $value;
                    }
                }
                unset($item);
            }
            if (isset($options['_line_item_properties']) && is_array($options['_line_item_properties'])) {
                foreach ($options['_line_item_properties'] as $k => $v) {
                    if (admin_norm_selected_option_label((string) $k) === $nl) {
                        $options['_line_item_properties'][$k] = $value;
                    }
                }
            }

            // Safe raw-key sync: only update a top-level (non-internal) option key whose
            // current stored value exactly equals the previous displayed value.
            if ($source !== 'schema' && $oldValue !== null && $oldValue !== '' && $value !== $oldValue) {
                foreach ($options as $ok => $ov) {
                    if (!is_string($ok) || $ok === '' || $ok[0] === '_') continue;
                    if (is_array($ov)) continue;
                    if ((string) $ov === $oldValue) {
                        $options[$ok] = $value;
                    }
                }
            }
        }
        return $options;
    }
}

if (!function_exists('admin_parse_line_item_properties_override')) {
    function admin_parse_line_item_properties_override($text) {
        $text = trim((string)$text);
        if ($text === '') return [];
        if ($text[0] === '{') {
            $decoded = json_decode($text, true);
            if (is_array($decoded)) {
                $out = [];
                foreach ($decoded as $k => $v) {
                    if (!is_string($k) || $k === '' || $v === null || $v === '') continue;
                    $out[$k] = is_scalar($v) ? (string)$v : json_encode($v);
                }
                return $out;
            }
        }
        $out = [];
        foreach (preg_split('/\r\n|\r|\n/', $text) as $line) {
            $line = trim($line);
            if ($line === '') continue;
            if (preg_match('/^([^:]+):(.+)$/', $line, $m)) {
                $k = trim($m[1]);
                $v = trim($m[2]);
                if ($k !== '') $out[$k] = $v;
            }
        }
        return $out;
    }
}

if (!function_exists('admin_norm_option_value')) {
    function admin_norm_option_value($s) {
        return strtolower(trim((string)$s));
    }
}

if (!function_exists('admin_norm_option_id')) {
    function admin_norm_option_id($s) {
        return admin_norm_option_value(str_replace('-', '_', (string)$s));
    }
}

if (!function_exists('admin_config_get_value')) {
    function admin_config_get_value(array $options, $key) {
        if (array_key_exists($key, $options)) return $options[$key];
        $target = admin_norm_option_id($key);
        foreach ($options as $k => $v) {
            if (!is_string($k) || $k === '' || $k[0] === '_') continue;
            if (admin_norm_option_id($k) === $target) return $v;
        }
        return null;
    }
}

if (!function_exists('admin_find_schema_choice')) {
    function admin_find_schema_choice(array $choices, $selected) {
        $selStr = (string)$selected;
        foreach ($choices as $c) {
            if (!is_array($c)) continue;
            if ((string)($c['value'] ?? '') === $selStr) return $c;
        }
        $selNorm = admin_norm_option_id($selStr);
        foreach ($choices as $c) {
            if (!is_array($c)) continue;
            if (admin_norm_option_id($c['value'] ?? '') === $selNorm) return $c;
        }
        return null;
    }
}

if (!function_exists('admin_normalize_price_overrides')) {
    function admin_normalize_price_overrides(array $choice) {
        $overrides = $choice['priceOverrides'] ?? null;
        if (is_array($overrides) && count($overrides) > 0) return $overrides;
        if (!isset($choice['priceOverridesRaw'])) {
            return is_array($overrides) ? $overrides : [];
        }
        $raw = $choice['priceOverridesRaw'];
        for ($i = 0; $i < 3; $i++) {
            if (is_array($raw)) return $raw;
            if (!is_string($raw) || trim($raw) === '') break;
            $decoded = json_decode($raw, true);
            if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) break;
            if ($decoded === $raw) break;
            $raw = $decoded;
        }
        return is_array($raw) ? $raw : [];
    }
}

if (!function_exists('admin_choice_field_truthy')) {
    function admin_choice_field_truthy(array $choice, $key) {
        $v = $choice[$key] ?? null;
        if ($v === null && is_string($key)) {
            $alt = strpos($key, '_') !== false ? str_replace('_', '-', $key) : str_replace('-', '_', $key);
            $v = $choice[$alt] ?? null;
        }
        if ($v === true) return true;
        if ($v === false || $v === null) return false;
        if (is_numeric($v)) return ((float)$v) !== 0.0;
        $s = admin_norm_option_value($v);
        if ($s === '' || $s === 'false' || $s === '0' || $s === 'no' || $s === 'off') return false;
        return true;
    }
}

if (!function_exists('admin_option_has_selection')) {
    function admin_option_has_selection($type, $selected) {
        $type = strtolower((string)$type);
        if ($type === 'checkbox') {
            return is_array($selected) && count($selected) > 0;
        }
        if (in_array($type, ['select', 'radio'], true)) {
            return $selected !== null && $selected !== '';
        }
        return false;
    }
}

if (!function_exists('admin_option_value_matches')) {
    function admin_option_value_matches(array $options, $key, $val) {
        $other = admin_config_get_value($options, (string)$key);
        $target = admin_norm_option_value($val);
        if (is_array($other)) {
            foreach ($other as $ov) {
                if (admin_norm_option_value($ov) === $target) return true;
            }
            return false;
        }
        return admin_norm_option_value($other ?? '') === $target;
    }
}

if (!function_exists('admin_option_is_panel_profile')) {
    function admin_option_is_panel_profile(array $opt) {
        $id = admin_norm_option_value(str_replace('-', '_', (string)($opt['id'] ?? '')));
        return $id === 'panel_profile';
    }
}

if (!function_exists('admin_option_is_stile_and_rail_profile')) {
    function admin_option_is_stile_and_rail_profile(array $opt) {
        $id = admin_norm_option_value(str_replace('-', '_', (string)($opt['id'] ?? '')));
        return $id === 'stile_and_rail_profile';
    }
}

if (!function_exists('admin_normalize_checkbox_selected_values')) {
    /**
     * Normalize stored checkbox selections to a list of choice values.
     * Empty / false-like values => unchecked (default).
     */
    function admin_normalize_checkbox_selected_values($current, array $choices = []) {
        if ($current === null || $current === false || $current === '' || $current === []) {
            return [];
        }
        $raw = [];
        if (is_array($current)) {
            $raw = $current;
        } else {
            $raw = [$current];
        }
        $choiceValues = [];
        foreach ($choices as $c) {
            if (!is_array($c)) continue;
            $cv = $c['value'] ?? '';
            if ($cv === '' && $cv !== 0 && $cv !== '0') continue;
            $choiceValues[] = (string)$cv;
        }
        $selected = [];
        foreach ($raw as $item) {
            if ($item === null || $item === false) continue;
            $norm = admin_norm_option_value($item);
            if ($norm === '' || $norm === 'false' || $norm === '0' || $norm === 'no' || $norm === 'off') {
                continue;
            }
            $itemStr = (string)$item;
            if ($choiceValues) {
                foreach ($choiceValues as $cv) {
                    if ($itemStr === $cv || admin_norm_option_value($cv) === $norm) {
                        $selected[] = $cv;
                        break;
                    }
                }
            } elseif ($norm === 'true' || $norm === '1' || $norm === 'yes' || $norm === 'on') {
                $selected[] = $itemStr;
            }
        }
        return array_values(array_unique($selected, SORT_REGULAR));
    }
}

if (!function_exists('admin_glass_bevel_is_enabled')) {
    function admin_glass_bevel_is_enabled(array $options) {
        if (admin_config_key_marked_enabled($options, 'glass_bevel')) {
            return true;
        }
        $val = admin_config_get_value($options, 'glass_bevel');
        if (is_array($val)) {
            foreach ($val as $x) {
                $n = admin_norm_option_value($x);
                if ($n !== '' && $n !== 'false' && $n !== '0' && $n !== 'no' && $n !== 'off') {
                    return true;
                }
            }
        } elseif ($val !== null && $val !== '') {
            $n = admin_norm_option_value($val);
            if ($n !== '' && $n !== 'false' && $n !== '0' && $n !== 'no' && $n !== 'off') {
                return true;
            }
        }
        $summary = $options['_selected_summary'] ?? null;
        if (is_array($summary)) {
            foreach ($summary as $item) {
                if (!is_array($item)) {
                    continue;
                }
                if (strcasecmp(trim((string)($item['label'] ?? '')), 'Bevel Glass') !== 0) {
                    continue;
                }
                $v = strtolower(trim((string)($item['value'] ?? '')));
                if ($v === '' || strpos($v, 'no bevel') !== false) {
                    return false;
                }
                if (strpos($v, 'add') !== false) {
                    return true;
                }
            }
        }
        return false;
    }
}

if (!function_exists('admin_read_post_option_value')) {
    function admin_read_post_option_value($oid) {
        $oid = (string)$oid;
        $keys = ['opt_' . $oid, 'opt_' . str_replace('-', '_', $oid), 'opt_' . str_replace('_', '-', $oid)];
        $keys = array_values(array_unique($keys, SORT_REGULAR));
        foreach ($keys as $key) {
            if (array_key_exists($key, $_POST)) {
                return $_POST[$key];
            }
        }
        return '';
    }
}

if (!function_exists('admin_select_value_matches')) {
    function admin_select_value_matches($current, $optionValue) {
        if ($current === null || $current === '') {
            return false;
        }
        if ((string)$current === (string)$optionValue) {
            return true;
        }
        if (is_numeric($current) && is_numeric($optionValue)) {
            return abs((float)$current - (float)$optionValue) < 0.000001;
        }
        $nc = admin_norm_option_value($current);
        $no = admin_norm_option_value($optionValue);
        if ($nc === $no) {
            return true;
        }
        // Machine tokens (satin_nickel) vs display labels (Satin Nickel).
        $nc2 = preg_replace('/[\s_\-]+/', ' ', $nc);
        $no2 = preg_replace('/[\s_\-]+/', ' ', $no);
        return $nc2 === $no2;
    }
}

if (!function_exists('admin_selected_option_field_is_selected')) {
    /** True when a metafield choice matches a saved field row (storage + display). */
    function admin_selected_option_field_is_selected(array $field, $choiceValue, $choiceLabel) {
        $display = trim((string) ($field['value'] ?? ''));
        $storage = trim((string) ($field['storage_value'] ?? ''));
        foreach (array_unique(array_filter([$storage, $display])) as $saved) {
            if (admin_select_value_matches($saved, $choiceValue)
                || admin_select_value_matches($saved, $choiceLabel)) {
                return true;
            }
        }
        return $display !== '' && ($choiceLabel === $display || admin_select_value_matches($display, $choiceLabel));
    }
}

if (!function_exists('admin_ensure_current_choice_in_list')) {
    /**
     * Guarantee the saved value appears in dropdown choices (metafield list may omit it).
     *
     * @return list<array{value: string, label: string}>
     */
    function admin_ensure_current_choice_in_list(array $choices, $storageValue, $displayValue) {
        $choices = admin_normalize_choice_list($choices);
        $probe = [
            'value'         => trim((string) $displayValue),
            'storage_value' => trim((string) $storageValue),
        ];
        foreach ($choices as $c) {
            if (!is_array($c)) {
                continue;
            }
            if (admin_selected_option_field_is_selected(
                $probe,
                (string) ($c['value'] ?? ''),
                (string) ($c['label'] ?? '')
            )) {
                return $choices;
            }
        }
        $storageValue = trim((string) $storageValue);
        $displayValue = trim((string) $displayValue);
        if ($storageValue !== '' || $displayValue !== '') {
            $choices[] = [
                'value' => $storageValue !== '' ? $storageValue : $displayValue,
                'label' => $displayValue !== '' ? $displayValue : $storageValue,
            ];
        }
        return $choices;
    }
}

if (!function_exists('admin_measurement_alias_groups')) {
    function admin_measurement_alias_groups() {
        return [
            ['exact_door_width_int', 'exact_door_width', 'door_width'],
            ['exact_door_width_frac', 'exact_door_width_fraction', 'door_width_fraction'],
            ['door_height', 'exact_door_height'],
            ['door_height_fraction', 'door_height_frac'],
            ['exact_door_thickness_frac', 'exact_door_thickness', 'door_thickness'],
            ['exact_pre_hung_frac', 'exact_pre_hung'],
            ['finished_width_int', 'finished_width'],
            ['finished_width_frac', 'finished_width_fraction'],
            ['finished_height_int', 'finished_height'],
            ['finished_height_frac', 'finished_height_fraction'],
            ['rough_width_int', 'rough_width'],
            ['rough_width_frac', 'rough_width_fraction'],
            ['rough_height_int', 'rough_height'],
            ['rough_height_frac', 'rough_height_fraction'],
            ['panel_width_int', 'panel_width'],
            ['panel_height_int', 'panel_height'],
            ['panel_height_frac', 'panel_height_fraction'],
            ['sidelight_width', 'sidelight_width_int'],
            ['sidelight_width_fraction', 'sidelight_width_frac'],
            ['panel_transom_height_int', 'panel_transom_height'],
            ['panel_transom_height_frac', 'panel_transom_height_fraction'],
        ];
    }
}

if (!function_exists('admin_measurement_keys_for_option_id')) {
    function admin_measurement_keys_for_option_id($optionId) {
        $norm = admin_norm_option_id($optionId);
        $keys = [(string)$optionId, $norm, str_replace('_', '-', $norm)];
        foreach (admin_measurement_alias_groups() as $group) {
            $groupNorm = array_map('admin_norm_option_id', $group);
            if (!in_array($norm, $groupNorm, true)) {
                continue;
            }
            foreach ($group as $g) {
                $keys[] = (string)$g;
                $keys[] = str_replace('_', '-', $g);
            }
        }
        return array_values(array_unique($keys, SORT_REGULAR));
    }
}

if (!function_exists('admin_config_get_measurement_value')) {
    function admin_config_get_measurement_value(array $options, $optionId) {
        foreach (admin_measurement_keys_for_option_id($optionId) as $key) {
            if (!array_key_exists($key, $options)) {
                continue;
            }
            $val = $options[$key];
            if ($val === null || $val === '') {
                continue;
            }
            return $val;
        }
        $fromNorm = admin_config_get_value($options, $optionId);
        if ($fromNorm !== null && $fromNorm !== '') {
            return $fromNorm;
        }
        return null;
    }
}

if (!function_exists('admin_collect_populated_measurement_ids')) {
    function admin_collect_populated_measurement_ids(array $options, array $schema) {
        $ids = [];
        foreach ($schema as $o) {
            if (!is_array($o) || empty($o['id']) || !admin_is_measurement_schema_option($o)) {
                continue;
            }
            $norm = admin_norm_option_id($o['id']);
            $val = admin_config_get_measurement_value($options, $o['id']);
            if ($val === null || $val === '') {
                continue;
            }
            $ids[] = $norm;
        }
        return array_values(array_unique($ids, SORT_REGULAR));
    }
}

if (!function_exists('admin_merge_summary_measurement_lines')) {
    function admin_merge_summary_measurement_lines(array $options, array $lines) {
        $existing = [];
        foreach ($lines as $line) {
            $existing[strtolower(trim((string)($line['label'] ?? '')))] = true;
        }
        $summary = $options['_selected_summary'] ?? null;
        if (!is_array($summary)) {
            return $lines;
        }
        foreach ($summary as $item) {
            if (!is_array($item)) {
                continue;
            }
            $label = trim((string)($item['label'] ?? ''));
            $value = trim((string)($item['value'] ?? ''));
            if ($label === '' || $value === '') {
                continue;
            }
            if (!preg_match('/(width|height|thick|pre.?hung|opening|jamb|sidelight|transom|measurement)/i', $label)) {
                continue;
            }
            $lk = strtolower($label);
            if (!empty($existing[$lk])) {
                continue;
            }
            $lines[] = ['label' => $label, 'value' => $value];
            $existing[$lk] = true;
        }
        return $lines;
    }
}

if (!function_exists('admin_merge_millwork_summary_lines')) {
    /**
     * Append Wood / Custom or Millwork Length / Custom Thickness to summary lines.
     * Used by Saved carts (folder_detail.php) and listing APIs — always surfaces
     * stored millwork values even when other summary lines already exist.
     */
    function admin_merge_millwork_summary_lines(array $options, array $lines) {
        if (!function_exists('admin_resolve_millwork_summary_field_rows')) {
            return $lines;
        }
        $existing = [];
        foreach ($lines as $line) {
            if (!is_array($line)) {
                continue;
            }
            $label = trim((string) ($line['label'] ?? ''));
            if ($label === '') {
                continue;
            }
            $existing[strtolower($label)] = true;
            $nl = admin_norm_selected_option_label($label);
            if ($nl !== '') {
                $existing[$nl] = true;
                $core = admin_selected_option_core_label($nl);
                if ($core !== '') {
                    $existing['core:' . $core] = true;
                }
            }
        }
        foreach (admin_resolve_millwork_summary_field_rows($options) as $field) {
            if (!is_array($field)) {
                continue;
            }
            $label = trim((string) ($field['label'] ?? ''));
            $value = trim((string) ($field['value'] ?? ''));
            if ($label === '' || $value === '') {
                continue;
            }
            $nl = admin_norm_selected_option_label($label);
            $lk = strtolower($label);
            $core = admin_selected_option_core_label($nl);
            if (!empty($existing[$lk]) || !empty($existing[$nl])
                || ($core !== '' && !empty($existing['core:' . $core]))) {
                continue;
            }
            $lines[] = ['label' => $label, 'value' => $value];
            $existing[$lk] = true;
            $existing[$nl] = true;
            if ($core !== '') {
                $existing['core:' . $core] = true;
            }
        }
        return $lines;
    }
}

if (!function_exists('admin_merge_resolved_selected_option_lines')) {
    /**
     * Append millwork / no-schema "Selected options" rows to saved-cart summary lines.
     * Uses the same resolver as the admin edit page so list counts match the editor.
     */
    function admin_merge_resolved_selected_option_lines(array $options, array $lines) {
        if (!function_exists('admin_resolve_selected_option_fields')) {
            return $lines;
        }
        $existing = [];
        foreach ($lines as $line) {
            if (!is_array($line)) {
                continue;
            }
            $label = trim((string) ($line['label'] ?? ''));
            if ($label === '') {
                continue;
            }
            $existing[strtolower($label)] = true;
            $nl = admin_norm_selected_option_label($label);
            if ($nl !== '') {
                $existing[$nl] = true;
                $core = admin_selected_option_core_label($nl);
                if ($core !== '') {
                    $existing['core:' . $core] = true;
                }
            }
        }
        foreach (admin_resolve_selected_option_fields($options) as $field) {
            if (!is_array($field)) {
                continue;
            }
            $label = trim((string) ($field['label'] ?? ''));
            $value = trim((string) ($field['value'] ?? ''));
            if ($label === '' || $value === '') {
                continue;
            }
            $nl = admin_norm_selected_option_label($label);
            $lk = strtolower($label);
            $core = admin_selected_option_core_label($nl);
            $entry = ['label' => $label, 'value' => $value];

            $replaced = false;
            foreach ($lines as &$line) {
                if (!is_array($line)) {
                    continue;
                }
                $lineNl = admin_norm_selected_option_label((string) ($line['label'] ?? ''));
                $lineCore = admin_selected_option_core_label($lineNl);
                if ($lineNl === $nl || strtolower((string) ($line['label'] ?? '')) === $lk
                    || ($core !== '' && $lineCore === $core)) {
                    $line['value'] = $value;
                    $replaced = true;
                    break;
                }
            }
            unset($line);
            if ($replaced) {
                $existing[$lk] = true;
                $existing[$nl] = true;
                if ($core !== '') {
                    $existing['core:' . $core] = true;
                }
                continue;
            }

            if (!empty($existing[$lk]) || !empty($existing[$nl])
                || ($core !== '' && !empty($existing['core:' . $core]))) {
                continue;
            }
            $lines[] = $entry;
            $existing[$lk] = true;
            $existing[$nl] = true;
            if ($core !== '') {
                $existing['core:' . $core] = true;
            }
        }
        return $lines;
    }
}

if (!function_exists('admin_sync_listing_selected_summary')) {
    /**
     * Merge millwork / no-schema selected options into _selected_summary so the
     * Saved carts project listing shows Wood, Length, Thickness (not only Quantity).
     */
    function admin_sync_listing_selected_summary(array $options) {
        if (!function_exists('admin_resolve_selected_option_fields')) {
            return $options;
        }
        if (!isset($options['_selected_summary']) || !is_array($options['_selected_summary'])) {
            $options['_selected_summary'] = [];
        }

        $byLabel = [];
        foreach ($options['_selected_summary'] as $idx => $item) {
            if (!is_array($item)) {
                continue;
            }
            $nl = admin_norm_selected_option_label((string) ($item['label'] ?? ''));
            if ($nl !== '') {
                $byLabel[$nl] = (int) $idx;
            }
        }

        foreach (admin_resolve_selected_option_fields($options) as $field) {
            if (!is_array($field)) {
                continue;
            }
            $label = trim((string) ($field['label'] ?? ''));
            $value = trim((string) ($field['value'] ?? ''));
            if ($label === '' || $value === '') {
                continue;
            }
            $nl = admin_norm_selected_option_label($label);
            $entry = [
                'label' => $label,
                'value' => $value,
                'image' => trim((string) ($field['image'] ?? '')),
            ];
            if (isset($byLabel[$nl])) {
                $options['_selected_summary'][$byLabel[$nl]] = array_merge(
                    is_array($options['_selected_summary'][$byLabel[$nl]])
                        ? $options['_selected_summary'][$byLabel[$nl]]
                        : [],
                    $entry
                );
            } else {
                $options['_selected_summary'][] = $entry;
                $byLabel[$nl] = count($options['_selected_summary']) - 1;
            }
        }

        return $options;
    }
}

if (!function_exists('admin_prepare_configuration_for_listing')) {
    /**
     * Enrich one configuration row for Saved carts / API listing (read-time).
     * Drop-in for configurations.php when returning folder configs.
     */
    function admin_prepare_configuration_for_listing(array $config) {
        $opts = json_decode((string) ($config['options_json'] ?? ''), true);
        if (!is_array($opts)) {
            $opts = [];
        }
        if (function_exists('admin_enrich_options_product_context')) {
            $opts = admin_enrich_options_product_context($opts, $config);
        }
        if (function_exists('admin_hydrate_hardware_selections_from_summary')) {
            $opts = admin_hydrate_hardware_selections_from_summary($opts);
        }
        $opts = admin_sync_listing_selected_summary($opts);
        $lines = admin_build_options_summary_lines($opts);
        $config['options'] = $opts;
        $config['options_json'] = json_encode($opts, JSON_UNESCAPED_UNICODE);
        $config['selected_options_summary'] = $lines;
        $config['selected_options_count'] = count($lines);
        return $config;
    }
}

if (!function_exists('admin_prepare_configurations_for_listing')) {
    /** @param list<array> $configs */
    function admin_prepare_configurations_for_listing(array $configs) {
        $out = [];
        foreach ($configs as $config) {
            if (!is_array($config)) {
                continue;
            }
            $out[] = admin_prepare_configuration_for_listing($config);
        }
        return $out;
    }
}

if (!function_exists('admin_config_key_marked_enabled')) {
    function admin_config_key_marked_enabled(array $options, $key) {
        $v = $options[$key] ?? null;
        if ($v === null || $v === '' || $v === false) return false;
        if (is_array($v)) {
            foreach ($v as $x) {
                $s = admin_norm_option_value($x);
                if ($s !== '' && $s !== 'false' && $s !== '0') return true;
            }
            return false;
        }
        $s = admin_norm_option_value($v);
        return $s !== '' && $s !== 'false' && $s !== '0';
    }
}

if (!function_exists('admin_panel_profile_pricing_gate_open')) {
    function admin_panel_profile_pricing_gate_open(array $options) {
        return admin_config_key_marked_enabled($options, 'panel_int_ext')
            || admin_config_key_marked_enabled($options, 'panel_storm_panel');
    }
}

if (!function_exists('admin_resolve_choice_price_amount')) {
  /**
   * Dollar delta for one schema choice (fixed or percent of base), including conditional overrides.
   * Mirrors door-conf2.js calculateRunningPricesPerOption / resolveChoicePrice.
   */
    function admin_resolve_choice_price_amount(?array $choice, array $opt, array $options, float $basePrice) {
        if (!$choice) return 0.0;
        if (admin_option_is_panel_profile($opt)) {
            $byChoice = admin_choice_field_truthy($choice, 'panel_int_ext')
                || admin_choice_field_truthy($choice, 'panel_storm_panel');
            if (!$byChoice && !admin_panel_profile_pricing_gate_open($options)) {
                return 0.0;
            }
        }
        $t = $choice['priceType'] ?? $choice['price_type'] ?? null;
        $v = (float)($choice['priceValue'] ?? $choice['price_value'] ?? 0);
        $overrides = admin_normalize_price_overrides($choice);

        if (!admin_option_is_stile_and_rail_profile($opt)) {
            if (count($overrides) > 0) {
                foreach ($overrides as $o) {
                    if (!is_array($o)) continue;
                    $matched = false;
                    $whenArr = $o['price_when'] ?? $o['priceWhen'] ?? null;
                    if (is_array($whenArr) && count($whenArr) > 0) {
                        $matched = true;
                        foreach ($whenArr as $cond) {
                            if (!is_array($cond)) { $matched = false; break; }
                            $k = $cond['key'] ?? $cond['price_when_key'] ?? '';
                            $cv = $cond['value'] ?? $cond['price_when_value'] ?? null;
                            if ($k === '' || $cv === null || !admin_option_value_matches($options, (string)$k, $cv)) {
                                $matched = false;
                                break;
                            }
                        }
                    } else {
                        $whenKey = $o['priceWhenKey'] ?? $o['price_when_key'] ?? '';
                        $whenVal = $o['priceWhenValue'] ?? $o['price_when_value'] ?? null;
                        if ($whenKey !== '' && $whenVal !== null && $whenVal !== '') {
                            $matched = admin_option_value_matches($options, (string)$whenKey, $whenVal);
                        }
                    }
                    if ($matched) {
                        $t = $o['priceWhenType'] ?? $o['price_when_type'] ?? 'fixed';
                        $v = (float)($o['priceWhenValueNumber'] ?? $o['price_when_value_number'] ?? 0);
                        break;
                    }
                }
            } else {
                $whenKey = $choice['priceWhenKey'] ?? $choice['price_when_key'] ?? '';
                $whenVal = $choice['priceWhenValue'] ?? $choice['price_when_value'] ?? null;
                if ($whenKey !== '' && $whenVal !== null && $whenVal !== '') {
                    if (admin_option_value_matches($options, (string)$whenKey, $whenVal)) {
                        $t = $choice['priceWhenType'] ?? $choice['price_when_type'] ?? 'fixed';
                        $v = (float)($choice['priceWhenValueNumber'] ?? $choice['price_when_value_number'] ?? 0);
                    }
                }
            }
        }

        if (!$t) $t = 'fixed';
        if ($t === 'fixed') return is_finite($v) ? round($v, 2) : 0.0;
        if ($t === 'percent') return is_finite($v) ? round($basePrice * $v, 2) : 0.0;
        return 0.0;
    }
}

if (!function_exists('admin_get_option_selected_price_delta')) {
    function admin_get_option_selected_price_delta(array $opt, array $options, float $basePrice) {
        $type = strtolower((string)($opt['type'] ?? $opt['input_type'] ?? ''));
        if (!in_array($type, ['select', 'radio', 'checkbox'], true)) return null;
        $choices = (isset($opt['options']) && is_array($opt['options'])) ? $opt['options'] : [];
        $oid = (string)($opt['id'] ?? '');
        $selected = admin_config_get_value($options, $oid);

        if ($type === 'select' || $type === 'radio') {
            if (!admin_option_has_selection($type, $selected)) return null;
            $ch = admin_find_schema_choice($choices, $selected);
            if (!$ch) return 0.0;
            return admin_resolve_choice_price_amount($ch, $opt, $options, $basePrice);
        }
        if ($type === 'checkbox') {
            $arr = admin_normalize_checkbox_selected_values($selected, $choices);
            if (!$arr) return null;
            $delta = 0.0;
            foreach ($arr as $val) {
                $ch = admin_find_schema_choice($choices, $val);
                if ($ch) {
                    $delta += admin_resolve_choice_price_amount($ch, $opt, $options, $basePrice);
                }
            }
            return round($delta, 2);
        }
        return null;
    }
}

if (!function_exists('admin_format_option_price_display')) {
    function admin_format_option_price_display($amount, $hasSelection = false) {
        if ($amount === null && !$hasSelection) return '';
        $n = round((float)($amount ?? 0), 2);
        if (!$hasSelection && abs($n) < 0.005) return '';
        $abs = abs($n);
        $s = number_format($abs, 2, '.', ',');
        return ($n >= 0 ? '+$' : '-$') . $s;
    }
}

if (!function_exists('admin_option_addon_breakdown_key_map')) {
    /** Schema option id → saved `_addon_breakdown` key (storefront API add-ons). */
    function admin_option_addon_breakdown_key_map() {
        return [
            'priming' => 'priming_services',
            'door_sweep' => 'door_sweeps_matching',
            'door_seal_kit' => 'door_seal_kit_matching',
        ];
    }
}

if (!function_exists('admin_measurement_addon_breakdown_keys_catalog')) {
    /**
     * Measurement-driven `_addon_breakdown` keys grouped by which dimension rows they apply to.
     *
     * @return array<string, list<string>>
     */
    function admin_measurement_addon_breakdown_keys_catalog() {
        return [
            'door_oversized' => [
                'int_ext_oversized',
                'screen_storm_oversized',
            ],
            'door_thickness' => [
                'interior_door_thickness',
                'screen_storm_thickness',
                'screen_storm_thickness_frac',
                'screen_storm_panel_door_thickness_extra',
                'sidelight_panel_thickness',
            ],
            'sidelight_transom' => [
                'sidelight_transom_oversized',
                'sidelight_transom_oversized_pair1',
                'sidelight_transom_oversized_pair2',
                'screen_storm_sidelight_transom',
                'screen_storm_transom_combo',
            ],
        ];
    }
}

if (!function_exists('admin_all_measurement_addon_breakdown_keys')) {
    function admin_all_measurement_addon_breakdown_keys() {
        $keys = [];
        foreach (admin_measurement_addon_breakdown_keys_catalog() as $groupKeys) {
            foreach ($groupKeys as $k) {
                $keys[$k] = true;
            }
        }
        return array_keys($keys);
    }
}

if (!function_exists('admin_sum_breakdown_keys')) {
    function admin_sum_breakdown_keys(array $options, array $keys) {
        $breakdown = isset($options['_addon_breakdown']) && is_array($options['_addon_breakdown'])
            ? $options['_addon_breakdown'] : [];
        $sum = 0.0;
        foreach ($keys as $key) {
            if (!array_key_exists($key, $breakdown) || !is_numeric($breakdown[$key])) {
                continue;
            }
            $sum += (float) $breakdown[$key];
        }
        if (abs($sum) < 0.005) {
            return null;
        }
        return round($sum, 2);
    }
}

if (!function_exists('admin_measurement_option_addon_group')) {
    function admin_measurement_option_addon_group($optionId) {
        $id = admin_norm_option_id($optionId);
        if ($id === 'measurement_type') {
            return 'all';
        }
        if (preg_match('/(sidelight.*width|sidelight_width|transom.*height|panel_transom|panel_door_width)/', $id)) {
            return 'sidelight_transom';
        }
        if (preg_match('/thick/', $id)) {
            return 'door_thickness';
        }
        if (preg_match('/(width|height|pre_hung|jamb)/', $id)) {
            return 'door_oversized';
        }
        return null;
    }
}

if (!function_exists('admin_get_measurement_type_addon_price')) {
    /** Combined measurement add-on total (all measurement-driven breakdown keys). */
    function admin_get_measurement_type_addon_price(array $options) {
        if (admin_resolve_measurement_type_value($options) === '') {
            return null;
        }
        return admin_sum_breakdown_keys($options, admin_all_measurement_addon_breakdown_keys());
    }
}

if (!function_exists('admin_config_option_has_value')) {
    function admin_config_option_has_value(array $options, $key) {
        $val = admin_config_get_value($options, $key);
        if (is_array($val)) {
            return count($val) > 0;
        }
        return $val !== null && $val !== '';
    }
}

if (!function_exists('admin_breakdown_sidelight_transom_split_mode')) {
    function admin_breakdown_sidelight_transom_split_mode(array $breakdown) {
        $pair1 = (float) ($breakdown['sidelight_transom_oversized_pair1'] ?? 0);
        $pair2 = (float) ($breakdown['sidelight_transom_oversized_pair2'] ?? 0);
        return abs($pair1) > 0.005 || abs($pair2) > 0.005;
    }
}

if (!function_exists('admin_sum_sidelight_transom_row_addon')) {
    /**
     * Sidelight vs transom chart add-on for one measurement row (not combined pair1+pair2).
     */
    function admin_sum_sidelight_transom_row_addon(array $options, $optionId) {
        $id = admin_norm_option_id($optionId);
        $breakdown = isset($options['_addon_breakdown']) && is_array($options['_addon_breakdown'])
            ? $options['_addon_breakdown'] : [];
        $pair1 = (float) ($breakdown['sidelight_transom_oversized_pair1'] ?? 0);
        $pair2 = (float) ($breakdown['sidelight_transom_oversized_pair2'] ?? 0);
        $legacy = (float) ($breakdown['sidelight_transom_oversized'] ?? 0);
        $split = admin_breakdown_sidelight_transom_split_mode($breakdown);
        $hasSl = admin_config_option_has_value($options, 'sidelight_style');
        $hasTr = admin_config_option_has_value($options, 'transom_style');
        $ssSl = (float) ($breakdown['screen_storm_sidelight_transom'] ?? 0);
        $ssTc = (float) ($breakdown['screen_storm_transom_combo'] ?? 0);

        if (preg_match('/(sidelight.*width|sidelight_width)/', $id)) {
            if (!$split && abs($legacy) > 0.005 && $hasTr && !$hasSl) {
                return null;
            }
            $amount = ($split ? $pair2 : $legacy) + $ssSl;
            return abs($amount) < 0.005 ? null : round($amount, 2);
        }
        if (preg_match('/(transom.*height|panel_transom|panel_door_width)/', $id)) {
            if (!$split && abs($legacy) > 0.005 && $hasSl && !$hasTr) {
                return null;
            }
            $amount = ($split ? $pair1 : $legacy) + $ssSl + $ssTc;
            return abs($amount) < 0.005 ? null : round($amount, 2);
        }
        return null;
    }
}

if (!function_exists('admin_measurement_addon_shows_on_option_row')) {
    /** Measurement API add-on price is shown once on measurement_type, not each dimension row. */
    function admin_measurement_addon_shows_on_option_row($optionId) {
        return admin_norm_option_id($optionId) === 'measurement_type';
    }
}

if (!function_exists('admin_get_measurement_option_addon_amount')) {
    function admin_get_measurement_option_addon_amount(array $options, $optionId) {
        $oid = admin_norm_option_id($optionId);
        if (!admin_measurement_addon_shows_on_option_row($oid)) {
            return null;
        }
        $group = admin_measurement_option_addon_group($oid);
        if ($group === null) {
            return null;
        }
        if ($group === 'all') {
            if (admin_resolve_measurement_type_value($options) === '') {
                return null;
            }
            return admin_get_measurement_type_addon_price($options);
        }
        return null;
    }
}

if (!function_exists('admin_is_measurement_option_id')) {
    function admin_is_measurement_option_id($optionId) {
        if (admin_is_millwork_selected_option_key($optionId)) {
            return false;
        }
        $id = admin_norm_option_id($optionId);
        if ($id === 'measurement_type') {
            return true;
        }
        return (bool) preg_match('/(width|height|thick|pre_hung|jamb|sidelight|transom)/', $id);
    }
}

if (!function_exists('admin_addon_breakdown_keys_on_option_rows')) {
    function admin_addon_breakdown_keys_on_option_rows() {
        return array_values(array_unique(array_merge(
            array_values(admin_option_addon_breakdown_key_map()),
            admin_all_measurement_addon_breakdown_keys()
        )));
    }
}

if (!function_exists('admin_get_option_addon_breakdown_price')) {
    /**
     * Price from saved storefront `_addon_breakdown` for options priced via metaobject API
     * (priming, door sweep, door seal kit).
     */
    function admin_get_option_addon_breakdown_price($optionId, array $options) {
        $map = admin_option_addon_breakdown_key_map();
        $oid = admin_norm_option_id($optionId);
        if (isset($map[$oid])) {
            $breakdown = isset($options['_addon_breakdown']) && is_array($options['_addon_breakdown'])
                ? $options['_addon_breakdown'] : [];
            $key = $map[$oid];
            if (!array_key_exists($key, $breakdown)) {
                return null;
            }
            return round((float) $breakdown[$key], 2);
        }

        if (!admin_is_measurement_option_id($oid)) {
            return null;
        }

        $group = admin_measurement_option_addon_group($oid);
        if ($group === null) {
            return null;
        }

        return admin_get_measurement_option_addon_amount($options, $oid);
    }
}

if (!function_exists('admin_get_option_display_price_delta')) {
    /** Schema choice price, or saved API add-on when the option uses `_addon_breakdown`. */
    function admin_get_option_display_price_delta(array $opt, array $options, float $basePrice) {
        $type = strtolower((string)($opt['type'] ?? $opt['input_type'] ?? ''));
        if (!in_array($type, ['select', 'radio', 'checkbox'], true)) {
            return null;
        }
        $oid = (string)($opt['id'] ?? '');
        $selected = admin_config_get_value($options, $oid);
        $hasSelection = ($type === 'checkbox')
            ? count(admin_normalize_checkbox_selected_values($selected, $opt['options'] ?? [])) > 0
            : admin_option_has_selection($type, $selected);
        if (!$hasSelection) {
            return null;
        }
        $addonDelta = admin_get_option_addon_breakdown_price($oid, $options);
        if ($addonDelta !== null) {
            return $addonDelta;
        }
        return admin_get_option_selected_price_delta($opt, $options, $basePrice);
    }
}

if (!function_exists('admin_get_order_total_price')) {
    function admin_get_order_total_price(array $options, float $unitPrice) {
        $qty = max(1, (int)($options['_quantity'] ?? 1));
        if (isset($options['_admin_order_total_override']) && $options['_admin_order_total_override'] !== '' && is_numeric($options['_admin_order_total_override'])) {
            return round((float)$options['_admin_order_total_override'], 2);
        }
        return round($unitPrice * $qty, 2);
    }
}

if (!function_exists('admin_addon_breakdown_labels')) {
  /** Human labels for storefront `_addon_breakdown` keys saved in options_json. */
  function admin_addon_breakdown_labels() {
    return [
      'glass_formula_sidelight' => 'Sidelight style (glass formula API)',
      'glass_formula_transom' => 'Transom style (glass formula API)',
      'sidelight_transom_oversized' => 'Sidelight / transom oversized (chart)',
      'sidelight_transom_oversized_pair1' => 'Sidelight / transom oversized (pair 1)',
      'sidelight_transom_oversized_pair2' => 'Sidelight / transom oversized (pair 2)',
      'storm_glass' => 'Storm glass API add-on',
      'stile_and_rail_profile' => 'Stile & rail profile add-on',
      'int_ext_oversized' => 'Oversized door (int/ext API)',
      'screen_storm_oversized' => 'Oversized door (screen/storm chart)',
      'screen_storm_thickness' => 'Screen/storm thickness add-on',
      'screen_storm_thickness_frac' => 'Screen/storm thickness fraction add-on',
      'screen_storm_sidelight_transom' => 'Screen/storm sidelight/transom add-on',
      'screen_storm_transom_combo' => 'Screen/storm transom combo add-on',
      'screen_storm_panel_door_thickness_extra' => 'Screen/storm panel door thickness extra',
      'interior_door_thickness' => 'Interior door thickness add-on',
      'sidelight_panel_thickness' => 'Sidelight panel thickness add-on',
      'door_seal_kit_matching' => 'Door seal kit',
      'door_sweeps_matching' => 'Door sweeps',
      'priming_services' => 'Priming services',
    ];
  }
}

if (!function_exists('admin_sum_addon_breakdown')) {
  function admin_sum_addon_breakdown(array $breakdown) {
    $sum = 0.0;
    foreach ($breakdown as $v) {
      if (is_numeric($v)) {
        $sum += (float) $v;
      }
    }
    return round($sum, 2);
  }
}

if (!function_exists('admin_get_applied_unit_price')) {
  /**
   * Unit price shown to customer: prefer stored `_applied_unit_price` from storefront save,
   * else computed_price column, else base price.
   */
  function admin_get_applied_unit_price(array $options, float $basePrice, ?float $computedPrice = null) {
    $unit = round((float) $basePrice, 2);
    if (isset($options['_applied_unit_price']) && $options['_applied_unit_price'] !== '' && is_numeric($options['_applied_unit_price'])) {
      $unit = round((float) $options['_applied_unit_price'], 2);
    } elseif ($computedPrice !== null && $computedPrice > $basePrice + 0.001) {
      $unit = round((float) $computedPrice, 2);
    }

    $breakdown = isset($options['_addon_breakdown']) && is_array($options['_addon_breakdown'])
      ? $options['_addon_breakdown'] : [];
    if ($breakdown) {
      $addonSum = admin_sum_addon_breakdown($breakdown);
      $reconstructed = round($basePrice + $addonSum, 2);
      if ($addonSum > 0.005 && $unit < $reconstructed - 0.001) {
        return $reconstructed;
      }
    }
    return $unit;
  }
}

if (!function_exists('admin_render_addon_breakdown_rows')) {
  function admin_render_addon_breakdown_rows(array $options) {
    $breakdown = isset($options['_addon_breakdown']) && is_array($options['_addon_breakdown'])
      ? $options['_addon_breakdown'] : [];
    if (!$breakdown) {
      return '';
    }
    $labels = admin_addon_breakdown_labels();
    $onOptionRows = admin_addon_breakdown_keys_on_option_rows();
    $html = '';
    $measAddon = admin_get_measurement_type_addon_price($options);
    if ($measAddon !== null && abs((float) $measAddon) > 0.005) {
      $html .= '<tr><td>Measurements</td>'
        . '<td class="admin-money">$' . htmlspecialchars(number_format((float) $measAddon, 2), ENT_QUOTES, 'UTF-8') . '</td></tr>';
    }
    foreach ($labels as $key => $label) {
      if (in_array($key, $onOptionRows, true)) {
        continue;
      }
      if (!array_key_exists($key, $breakdown)) {
        continue;
      }
      $amt = (float) $breakdown[$key];
      if (abs($amt) < 0.005) {
        continue;
      }
      $html .= '<tr><td>' . htmlspecialchars($label, ENT_QUOTES, 'UTF-8') . '</td>'
        . '<td class="admin-money">$' . htmlspecialchars(number_format($amt, 2), ENT_QUOTES, 'UTF-8') . '</td></tr>';
    }
    return $html;
  }
}

if (!function_exists('admin_norm_property_key')) {
    function admin_norm_property_key($key) {
        return strtolower(preg_replace('/\s+/', ' ', trim((string)$key)));
    }
}

if (!function_exists('admin_normalize_measurement_type_value')) {
    function admin_normalize_measurement_type_value($value) {
        $t = admin_norm_option_id($value);
        if ($t === 'finished_opening') return 'finished_opening_size';
        if ($t === 'jamb_unit_size') return 'pre_hung_unit_size';
        return $t;
    }
}

if (!function_exists('admin_options_value_equals')) {
    function admin_options_value_equals(array $options, $target) {
        $target = admin_norm_option_id($target);
        foreach ($options as $k => $v) {
            if (!is_string($k) || $k === '' || $k[0] === '_') continue;
            if (is_array($v)) {
                foreach ($v as $x) {
                    if (admin_norm_option_id($x) === $target) return true;
                }
            } elseif (admin_norm_option_id($v) === $target) {
                return true;
            }
        }
        return false;
    }
}

if (!function_exists('admin_get_measurement_setup_combo_rules')) {
    function admin_get_measurement_setup_combo_rules() {
        return [
            ['setup' => 'slab_only', 'measure' => 'exact_door_size', 'ids' => [
                'exact_door_width_int', 'exact_door_width_frac', 'door_height', 'door_height_fraction', 'exact_door_thickness_frac',
            ]],
            ['setup' => 'pre_hung_on_jamb', 'measure' => 'exact_door_size', 'ids' => [
                'exact_door_width_int', 'exact_door_width_frac', 'door_height', 'door_height_fraction', 'exact_door_thickness_frac', 'exact_pre_hung_frac',
            ]],
        ];
    }
}

if (!function_exists('admin_get_measurement_type_field_map')) {
    /** measurement_type value => option ids allowed on line items / summaries. */
    function admin_get_measurement_type_field_map() {
        return [
            'finished_opening_size' => [
                'finished_width_int', 'finished_width_frac', 'finished_height_int', 'finished_height_frac', 'door_thickness',
            ],
            'pre_hung_unit_size' => [
                'exact_pre_hung_frac', 'rough_pre_hung_int', 'panel_pre_hung_int', 'panel_pre_hung_frac',
                'jamb_pre_hung_int', 'jamb_pre_hung_frac',
            ],
            'rough_opening_size' => [
                'rough_width_int', 'rough_width_frac', 'rough_height_int', 'rough_height_frac',
                'panel_width_int', 'panel_height_int', 'panel_height_frac',
            ],
            'exact_door_size' => [
                'exact_door_width_int', 'exact_door_width_frac', 'door_height', 'door_height_fraction', 'exact_door_thickness_frac',
            ],
            'exact_sidelight_size' => [
                'sidelight_width', 'sidelight_width_fraction',
            ],
            'exact_transom_size' => [
                'panel_transom_height_int', 'panel_transom_height_frac',
            ],
        ];
    }
}

if (!function_exists('admin_is_measurement_schema_option')) {
    function admin_is_measurement_schema_option(array $opt) {
        if (($opt['_adminGroup'] ?? '') === 'measurement') return true;
        $id = admin_norm_option_id($opt['id'] ?? '');
        if ($id === 'measurement_type') return false;
        return (bool)preg_match('/(width|height|thick|pre_hung|jamb|sidelight|transom)/', $id);
    }
}

if (!function_exists('admin_is_hidden_admin_fraction_field')) {
    /**
     * Fraction sub-dropdowns intentionally removed from the admin saved-cart UI:
     *   - Exact Door Width (fraction)
     *   - Exact Sidelight Width (fraction)
     *   - Exact Transom Height (fraction)
     * Door Height (fraction) and Door Thickness (fraction) are kept.
     * The stored value is preserved (the save handler skips these so it is not wiped).
     */
    function admin_is_hidden_admin_fraction_field($opt) {
        if (!is_array($opt)) return false;
        $id = strtolower(str_replace('-', '_', (string)($opt['id'] ?? '')));
        $label = strtolower((string)($opt['label'] ?? ''));
        $hay = $id . ' ' . $label;
        $isFrac = (strpos($id, 'frac') !== false) || (strpos($label, 'fraction') !== false);
        if (!$isFrac) return false;
        $isSidelight = (strpos($hay, 'sidelight') !== false || strpos($hay, 'side_light') !== false || strpos($hay, 'sidelite') !== false);
        $isTransom = (strpos($hay, 'transom') !== false);
        $hasWidth = (strpos($hay, 'width') !== false);
        $hasHeight = (strpos($hay, 'height') !== false);
        if ($isSidelight) return $hasWidth;   // Exact Sidelight Width (fraction)
        if ($isTransom) return $hasHeight;     // Exact Transom Height (fraction)
        return $hasWidth;                      // Exact Door Width (fraction)
    }
}

if (!function_exists('admin_resolve_measurement_type_value')) {
    /**
     * Saved measurement_type, or infer from populated dimension fields (storefront may omit type key).
     */
    function admin_resolve_measurement_type_value(array $options) {
        $raw = admin_config_get_value($options, 'measurement_type');
        if (is_array($raw)) {
            foreach ($raw as $v) {
                if ($v === null || $v === '') {
                    continue;
                }
                $normalized = admin_normalize_measurement_type_value($v);
                if ($normalized !== '') {
                    return $normalized;
                }
            }
        } elseif ($raw !== null && $raw !== '') {
            $normalized = admin_normalize_measurement_type_value($raw);
            if ($normalized !== '') {
                return $normalized;
            }
        }

        foreach (['exact_door_size', 'finished_opening_size', 'rough_opening_size', 'pre_hung_unit_size', 'exact_sidelight_size', 'exact_transom_size'] as $candidate) {
            $map = admin_get_measurement_type_field_map();
            if (!isset($map[$candidate])) {
                continue;
            }
            foreach (array_map('admin_norm_option_id', $map[$candidate]) as $pid) {
                if (admin_config_get_measurement_value($options, $pid) !== null) {
                    return $candidate;
                }
            }
        }
        return '';
    }
}

if (!function_exists('admin_get_active_measurement_option_ids')) {
    /**
     * Normalized option ids for the selected measurement_type (+ door setup combo for exact door).
     * Empty array = no measurement dimension fields should be exported.
     */
    function admin_get_active_measurement_option_ids(array $options) {
        $measureType = admin_resolve_measurement_type_value($options);
        if ($measureType === '') {
            return [];
        }

        if ($measureType === 'exact_door_size') {
            if (admin_options_value_equals($options, 'pre_hung_on_jamb')) {
                foreach (admin_get_measurement_setup_combo_rules() as $rule) {
                    if ($rule['measure'] === 'exact_door_size' && $rule['setup'] === 'pre_hung_on_jamb') {
                        return array_map('admin_norm_option_id', $rule['ids']);
                    }
                }
            }
            if (admin_options_value_equals($options, 'slab_only')) {
                foreach (admin_get_measurement_setup_combo_rules() as $rule) {
                    if ($rule['measure'] === 'exact_door_size' && $rule['setup'] === 'slab_only') {
                        return array_map('admin_norm_option_id', $rule['ids']);
                    }
                }
            }
        }

        $map = admin_get_measurement_type_field_map();
        if (!isset($map[$measureType])) return [];
        return array_map('admin_norm_option_id', $map[$measureType]);
    }
}

if (!function_exists('admin_schema_option_allowed_in_line_item')) {
    function admin_schema_option_allowed_in_line_item(array $opt, array $options) {
        $id = admin_norm_option_id($opt['id'] ?? '');
        if ($id === 'measurement_type') return true;
        if (!admin_is_measurement_schema_option($opt)) return true;
        $allowed = admin_get_active_measurement_option_ids($options);
        if (!$allowed) return false;
        return in_array($id, $allowed, true);
    }
}

if (!function_exists('admin_schema_choice_label')) {
    function admin_schema_choice_label(array $opt, $val) {
        $opts = (isset($opt['options']) && is_array($opt['options'])) ? $opt['options'] : [];
        foreach ($opts as $c) {
            if (is_array($c) && isset($c['value']) && (string)$c['value'] === (string)$val) {
                return (string)($c['label'] ?? $c['value']);
            }
        }
        return (string)$val;
    }
}

if (!function_exists('admin_format_schema_option_value_for_line_item')) {
    function admin_format_schema_option_value_for_line_item(array $opt, array $options) {
        $id = (string)($opt['id'] ?? '');
        $type = strtolower((string)($opt['type'] ?? $opt['input_type'] ?? ''));
        if ($type === 'hardware_multi' || $type === 'casing_multi') return null;
        if ($id === 'glass_bevel') {
            return admin_glass_bevel_is_enabled($options) ? 'Add Bevel Glass' : null;
        }
        // Measurement dimensions are stored under aliased keys (e.g. door_width vs
        // exact_door_width vs exact_door_width_int). Resolve them via the alias-group
        // getter (same as the edit form / summary), otherwise the saved value is dropped
        // from the draft order line item detail.
        $value = admin_is_measurement_schema_option($opt)
            ? admin_config_get_measurement_value($options, $id)
            : admin_config_get_value($options, $id);
        if ($value === null || $value === '' || (is_array($value) && !$value)) return null;
        if ($type === 'select' && isset($opt['options']) && is_array($opt['options'])) {
            $value = admin_schema_choice_label($opt, $value);
        } elseif (is_array($value)) {
            $labels = [];
            foreach ($value as $vv) {
                $labels[] = admin_schema_choice_label($opt, $vv);
            }
            $value = implode(', ', $labels);
        }
        return (string)$value;
    }
}

if (!function_exists('admin_build_schema_line_item_custom_attributes')) {
    function admin_build_schema_line_item_custom_attributes(array $options, int $configId = 0) {
        $attrs = [];
        if ($configId > 0) {
            $attrs[] = ['key' => 'Config ID', 'value' => (string)$configId];
        }
        $schema = (isset($options['_pricing_schema']) && is_array($options['_pricing_schema'])) ? $options['_pricing_schema'] : [];
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id']) || empty($opt['label'])) continue;
            $type = strtolower((string)($opt['type'] ?? $opt['input_type'] ?? ''));
            if ($type === '') continue;
            if (!admin_schema_option_allowed_in_line_item($opt, $options)) continue;
            $id = (string)$opt['id'];
            $label = $id === 'glass_bevel' ? 'Bevel Glass' : (string)$opt['label'];
            $value = admin_format_schema_option_value_for_line_item($opt, $options);
            if ($value === null || $value === '') continue;
            $attrs[] = ['key' => $label, 'value' => $value];
        }
        $quoteAttr = admin_build_quote_description_line_item_attribute($options);
        if ($quoteAttr !== null) {
            $attrs[] = $quoteAttr;
        }
        return $attrs;
    }
}

if (!function_exists('admin_build_options_summary_lines')) {
    function admin_build_options_summary_lines(array $options) {
        if (function_exists('admin_sync_listing_selected_summary')) {
            $options = admin_sync_listing_selected_summary($options);
        }
        $schema = (isset($options['_pricing_schema']) && is_array($options['_pricing_schema'])) ? $options['_pricing_schema'] : [];
        $norm = function ($s) { return admin_norm_option_id($s); };
        $choiceLabel = function ($o, $val) {
            return admin_schema_choice_label($o, $val);
        };
        $activeIds = admin_get_active_measurement_option_ids($options);
        $byId = [];
        foreach ($schema as $o) { if (is_array($o) && !empty($o['id'])) $byId[$norm($o['id'])] = $o; }
        $lines = [];
        foreach ($schema as $o) {
            if (!is_array($o) || empty($o['id'])) continue;
            $id = $norm($o['id']);
            if ($id === 'glass_bevel') {
                $lines[] = ['label' => 'Bevel Glass', 'value' => admin_glass_bevel_is_enabled($options) ? 'Add Bevel Glass' : 'No Bevel Glass'];
                continue;
            }
            if ($id === 'hardware_options' || $id === 'casing_options') continue;
            if (function_exists('admin_is_hardware_landing_schema_id') && admin_is_hardware_landing_schema_id($o['id'])) continue;
            if (admin_is_measurement_schema_option($o)) continue;
            $label = (string)($o['label'] ?? $o['id']);
            $val = admin_config_get_value($options, $o['id']);
            if ($val === null || $val === '' || (is_array($val) && !$val)) continue;
            if (is_array($val)) {
                $labels = [];
                foreach ($val as $vv) { $labels[] = $choiceLabel($o, $vv); }
                $lines[] = ['label' => $label, 'value' => implode(', ', $labels)];
            } else {
                $displayVal = $choiceLabel($o, $val);
                if (function_exists('admin_resolve_field_choices_for_label')) {
                    $schemaChoices = (!empty($o['options']) && is_array($o['options'])) ? $o['options'] : [];
                    $fieldChoices = admin_resolve_field_choices_for_label($options, $label, $schemaChoices);
                    $pair = admin_resolve_choice_pair((string) $val, $fieldChoices);
                    if (($pair['label'] ?? '') !== '') {
                        $displayVal = (string) $pair['label'];
                    }
                }
                $lines[] = ['label' => $label, 'value' => $displayVal];
            }
        }
        if ($activeIds) {
            foreach ($activeIds as $midNorm) {
                $o = $byId[$midNorm] ?? null;
                $val = $o ? admin_config_get_measurement_value($options, $o['id']) : admin_config_get_measurement_value($options, $midNorm);
                if ($val === null || $val === '') {
                    continue;
                }
                $label = $o ? (string)($o['label'] ?? $o['id']) : humanize_token($midNorm);
                $display = $o ? $choiceLabel($o, $val) : (string)$val;
                $lines[] = ['label' => $label, 'value' => $display];
            }
        } else {
            foreach (admin_collect_populated_measurement_ids($options, $schema) as $midNorm) {
                $o = $byId[$midNorm] ?? null;
                $val = $o ? admin_config_get_measurement_value($options, $o['id']) : admin_config_get_measurement_value($options, $midNorm);
                if ($val === null || $val === '') {
                    continue;
                }
                $label = $o ? (string)($o['label'] ?? $o['id']) : humanize_token($midNorm);
                $display = $o ? $choiceLabel($o, $val) : (string)$val;
                $lines[] = ['label' => $label, 'value' => $display];
            }
        }
        foreach (admin_hardware_summary_parts($options) as $hwPart) {
            if (preg_match('/^([^:]+):(.+)$/', $hwPart, $m)) {
                $lines[] = ['label' => trim($m[1]), 'value' => trim($m[2])];
            }
        }
        foreach (admin_casing_summary_parts($options) as $casingPart) {
            if (preg_match('/^([^:]+):(.+)$/', $casingPart, $m)) {
                $lines[] = ['label' => trim($m[1]), 'value' => trim($m[2])];
            }
        }
        $qty = (int)($options['_quantity'] ?? 0);
        if ($qty > 0) {
            $lines[] = ['label' => 'Quantity', 'value' => (string)$qty];
        }
        $lines = admin_merge_summary_measurement_lines($options, $lines);
        $lines = admin_merge_millwork_summary_lines($options, $lines);
        $lines = admin_merge_resolved_selected_option_lines($options, $lines);
        if (!empty($lines)) {
            return $lines;
        }
        $summary = $options['_selected_summary'] ?? null;
        if (is_array($summary)) {
            foreach ($summary as $item) {
                if (!is_array($item)) continue;
                $label = trim((string)($item['label'] ?? ''));
                $value = trim((string)($item['value'] ?? ''));
                if ($label !== '' && $value !== '') {
                    $lines[] = ['label' => $label, 'value' => $value];
                }
            }
        }
        if (!empty($lines)) {
            return $lines;
        }
        foreach ($options as $k => $v) {
            if (!is_string($k) || $k === '' || $k[0] === '_') continue;
            if ($k === 'hardware_options' || $k === 'casing_options') continue;
            if ($v === null || $v === '' || (is_array($v) && empty($v))) continue;
            if (is_array($v) && isset($v[0]) && is_array($v[0])) continue;
            if (is_array($v)) {
                $display = implode(', ', array_map(function ($x) {
                    return humanize_token((string)$x);
                }, $v));
            } else {
                $display = humanize_token((string)$v);
            }
            $lines[] = ['label' => humanize_token($k), 'value' => $display];
        }
        return $lines;
    }
}

if (!function_exists('admin_measurement_token_suffix')) {
    /** Map measurement option id => compact token suffix (display-only in quote preview/PDF). */
    function admin_measurement_token_suffix($optionId) {
        $id = admin_norm_option_id((string) $optionId);
        static $map = null;
        if ($map === null) {
            $map = [
                'exact_door_width_int' => 'w',
                'finished_width_int' => 'w',
                'rough_width_int' => 'w',
                'panel_width_int' => 'w',
                'sidelight_width' => 'w',
                'exact_door_width_frac' => 'wf',
                'finished_width_frac' => 'wf',
                'rough_width_frac' => 'wf',
                'sidelight_width_fraction' => 'wf',
                'door_height' => 'h',
                'finished_height_int' => 'h',
                'rough_height_int' => 'h',
                'panel_height_int' => 'h',
                'panel_transom_height_int' => 'h',
                'door_height_fraction' => 'hf',
                'finished_height_frac' => 'hf',
                'rough_height_frac' => 'hf',
                'panel_height_frac' => 'hf',
                'panel_transom_height_frac' => 'hf',
                'exact_door_thickness_frac' => 't',
                'door_thickness' => 't',
                'exact_pre_hung_frac' => 'ph',
                'rough_pre_hung_int' => 'ph',
                'panel_pre_hung_int' => 'ph',
                'panel_pre_hung_frac' => 'phf',
                'jamb_pre_hung_int' => 'jh',
                'jamb_pre_hung_frac' => 'jhf',
            ];
        }
        return $map[$id] ?? null;
    }
}

if (!function_exists('admin_get_measurement_type_display_label')) {
    function admin_get_measurement_type_display_label(array $options) {
        $schema = (isset($options['_pricing_schema']) && is_array($options['_pricing_schema'])) ? $options['_pricing_schema'] : [];
        foreach ($schema as $o) {
            if (!is_array($o) || empty($o['id'])) {
                continue;
            }
            if (admin_norm_option_id($o['id']) !== 'measurement_type') {
                continue;
            }
            $val = admin_config_get_value($options, $o['id']);
            if ($val === null || $val === '' || (is_array($val) && !$val)) {
                break;
            }
            if (is_array($val)) {
                $labels = [];
                foreach ($val as $vv) {
                    $labels[] = admin_schema_choice_label($o, $vv);
                }
                $label = implode(', ', $labels);
            } else {
                $label = admin_schema_choice_label($o, $val);
            }
            $label = trim((string) $label);
            if ($label !== '') {
                return $label;
            }
        }
        $measureType = admin_normalize_measurement_type_value($options['measurement_type'] ?? '');
        return $measureType !== '' ? humanize_token($measureType) : 'Measurements';
    }
}

if (!function_exists('admin_is_quote_measurement_label')) {
    function admin_is_quote_measurement_label($label) {
        $labelLower = strtolower((string) $label);
        if (strpos($labelLower, 'door') === false) {
            return false;
        }
        return strpos($labelLower, 'width') !== false
            || strpos($labelLower, 'height') !== false
            || strpos($labelLower, 'thickness') !== false
            || strpos($labelLower, 'thick') !== false;
    }
}

if (!function_exists('admin_is_quote_measurement_token')) {
    function admin_is_quote_measurement_token($text) {
        return (bool) preg_match('/^\d+(wf|w|h|hf|t)$/', trim((string) $text));
    }
}

if (!function_exists('admin_format_quote_description_display_value')) {
    /**
     * @return array{value: string, is_measurement: bool}
     */
    function admin_format_quote_description_display_value($label, $value, $optionId = '') {
        $value = trim((string) $value);
        if ($value === '') {
            return ['value' => '', 'is_measurement' => false];
        }

        $suffix = admin_measurement_token_suffix($optionId);
        if ($suffix !== null) {
            return ['value' => $value . $suffix, 'is_measurement' => true];
        }

        if (!admin_is_quote_measurement_label($label)) {
            return ['value' => $value, 'is_measurement' => false];
        }

        $labelLower = strtolower((string) $label);
        $hasWidth = strpos($labelLower, 'width') !== false;
        $hasHeight = strpos($labelLower, 'height') !== false;
        $hasThickness = strpos($labelLower, 'thickness') !== false || strpos($labelLower, 'thick') !== false;
        $hasFraction = strpos($labelLower, 'fraction') !== false;

        if ($hasWidth && $hasFraction) {
            return ['value' => $value . 'wf', 'is_measurement' => true];
        }
        if ($hasWidth) {
            return ['value' => $value . 'w', 'is_measurement' => true];
        }
        if ($hasHeight && $hasFraction) {
            return ['value' => $value . 'hf', 'is_measurement' => true];
        }
        if ($hasHeight) {
            return ['value' => $value . 'h', 'is_measurement' => true];
        }
        if ($hasThickness) {
            return ['value' => $value . 't', 'is_measurement' => true];
        }

        return ['value' => $value, 'is_measurement' => false];
    }
}

if (!function_exists('admin_quote_description_segment_id')) {
    function admin_quote_description_segment_id($id, $label = '') {
        $id = admin_norm_option_id((string) $id);
        if ($id !== '') {
            return $id;
        }
        $label = admin_norm_option_id((string) $label);
        return $label !== '' ? ('label:' . $label) : '';
    }
}

if (!function_exists('admin_build_measurement_quote_segments')) {
    /**
     * Measurement type label followed by compact tokens (e.g. Exact Door Size, 38w, 0wf, 82h).
     *
     * @return array<int, array{id: string, label: string, value: string, display_value: string, is_measurement: bool}>
     */
    function admin_build_measurement_quote_segments(array $options, array $byId, array $activeIds, callable $choiceLabel) {
        $activeIds = array_values(array_filter(array_map('admin_norm_option_id', $activeIds)));
        if (empty($activeIds)) {
            $schema = (isset($options['_pricing_schema']) && is_array($options['_pricing_schema'])) ? $options['_pricing_schema'] : [];
            $activeIds = admin_collect_populated_measurement_ids($options, $schema);
        }
        if (empty($activeIds)) {
            return [];
        }

        $segments = [];
        $typeLabel = admin_get_measurement_type_display_label($options);
        if ($typeLabel !== '') {
            $segments[] = [
                'id'             => 'measurement_type',
                'label'          => 'Measurement type',
                'value'          => $typeLabel,
                'display_value'  => $typeLabel,
                'is_measurement' => false,
            ];
        }

        foreach ($activeIds as $midNorm) {
            $o = $byId[$midNorm] ?? null;
            $val = $o ? admin_config_get_measurement_value($options, $o['id']) : admin_config_get_measurement_value($options, $midNorm);
            if ($val === null || $val === '') {
                continue;
            }
            $fieldLabel = $o ? (string) ($o['label'] ?? $o['id']) : humanize_token($midNorm);
            $display = $o ? $choiceLabel($o, $val) : (string) $val;
            $formatted = admin_format_quote_description_display_value($fieldLabel, $display, $midNorm);
            if ($formatted['value'] === '') {
                continue;
            }
            $segments[] = [
                'id'             => '_meas:' . $midNorm,
                'label'          => $fieldLabel,
                'value'          => (string) $display,
                'display_value'  => $formatted['value'],
                'is_measurement' => true,
            ];
        }

        return $segments;
    }
}

if (!function_exists('admin_inline_measurement_quote_segments')) {
    function admin_inline_measurement_quote_segments(array $segments, array $measurementSegments) {
        if (empty($measurementSegments)) {
            return array_values(array_filter($segments, function ($seg) {
                return empty($seg['__measurement_block__']);
            }));
        }
        $out = [];
        $inserted = false;
        foreach ($segments as $seg) {
            if (!empty($seg['__measurement_block__'])) {
                foreach ($measurementSegments as $m) {
                    $out[] = $m;
                }
                $inserted = true;
                continue;
            }
            $out[] = $seg;
        }
        if (!$inserted) {
            foreach ($measurementSegments as $m) {
                $out[] = $m;
            }
        }
        return $out;
    }
}

if (!function_exists('admin_build_quote_description_segments')) {
    /**
     * Ordered quote description segments with stable ids for staff overrides.
     *
     * @return array<int, array{id: string, label: string, value: string, display_value: string, is_measurement: bool}>
     */
    function admin_build_quote_description_segments(array $options) {
        $schema = (isset($options['_pricing_schema']) && is_array($options['_pricing_schema'])) ? $options['_pricing_schema'] : [];
        $norm = function ($s) { return admin_norm_option_id($s); };
        $choiceLabel = function ($o, $val) {
            return admin_schema_choice_label($o, $val);
        };
        $activeIds = admin_get_active_measurement_option_ids($options);
        $byId = [];
        foreach ($schema as $o) {
            if (is_array($o) && !empty($o['id'])) {
                $byId[$norm($o['id'])] = $o;
            }
        }

        $segments = [];
        $push = function ($id, $label, $value, $optionId = '') use (&$segments) {
            $value = trim((string) $value);
            if ($value === '') {
                return;
            }
            $segmentId = admin_quote_description_segment_id($id, $label);
            if ($segmentId === '') {
                return;
            }
            $formatted = admin_format_quote_description_display_value($label, $value, $optionId);
            $segments[] = [
                'id'             => $segmentId,
                'label'          => trim((string) $label),
                'value'          => $value,
                'display_value'  => $formatted['value'],
                'is_measurement' => $formatted['is_measurement'],
            ];
        };

        $measurementSegments = admin_build_measurement_quote_segments($options, $byId, $activeIds, $choiceLabel);

        foreach ($schema as $o) {
            if (!is_array($o) || empty($o['id'])) {
                continue;
            }
            $id = $norm($o['id']);
            if ($id === 'measurement_type') {
                $segments[] = ['__measurement_block__' => true];
                continue;
            }
            if ($id === 'glass_bevel') {
                $push('glass_bevel', 'Bevel Glass', admin_glass_bevel_is_enabled($options) ? 'Add Bevel Glass' : 'No Bevel Glass');
                continue;
            }
            if ($id === 'hardware_options' || $id === 'casing_options') {
                continue;
            }
            if (admin_is_measurement_schema_option($o)) {
                continue;
            }
            $label = (string) ($o['label'] ?? $o['id']);
            $val = admin_config_get_value($options, $o['id']);
            if ($val === null || $val === '' || (is_array($val) && !$val)) {
                continue;
            }
            if (is_array($val)) {
                $labels = [];
                foreach ($val as $vv) {
                    $labels[] = $choiceLabel($o, $vv);
                }
                $push($o['id'], $label, implode(', ', $labels));
            } else {
                $push($o['id'], $label, $choiceLabel($o, $val));
            }
        }

        $segments = admin_inline_measurement_quote_segments($segments, $measurementSegments);

        foreach (admin_hardware_summary_parts($options) as $hwPart) {
            if (preg_match('/^([^:]+):(.+)$/', $hwPart, $m)) {
                $label = trim($m[1]);
                $push('hw:' . admin_norm_option_id($label), $label, trim($m[2]));
            }
        }
        foreach (admin_casing_summary_parts($options) as $casingPart) {
            if (preg_match('/^([^:]+):(.+)$/', $casingPart, $m)) {
                $label = trim($m[1]);
                $push('casing:' . admin_norm_option_id($label), $label, trim($m[2]));
            }
        }

        $qty = (int) ($options['_quantity'] ?? 0);
        if ($qty > 0) {
            $push('_quantity', 'Quantity', (string) $qty);
        }

        if (function_exists('admin_resolve_selected_option_fields')) {
            foreach (admin_resolve_selected_option_fields($options) as $field) {
                if (!is_array($field)) {
                    continue;
                }
                $label = trim((string) ($field['label'] ?? ''));
                $value = trim((string) ($field['value'] ?? ''));
                if ($label === '' || $value === '') {
                    continue;
                }
                $fid = trim((string) ($field['id'] ?? ''));
                if ($fid === '') {
                    $fid = admin_norm_option_id($label);
                }
                $push('selected:' . $fid, $label, $value, $fid);
            }
        }

        if (!empty($segments)) {
            return $segments;
        }

        $summary = $options['_selected_summary'] ?? null;
        if (is_array($summary)) {
            foreach ($summary as $item) {
                if (!is_array($item)) {
                    continue;
                }
                $label = trim((string) ($item['label'] ?? ''));
                $value = trim((string) ($item['value'] ?? ''));
                if ($label !== '' && $value !== '') {
                    $push('summary:' . admin_norm_option_id($label), $label, $value);
                }
            }
        }
        if (!empty($segments)) {
            return $segments;
        }

        foreach ($options as $k => $v) {
            if (!is_string($k) || $k === '' || $k[0] === '_') {
                continue;
            }
            if ($k === 'hardware_options' || $k === 'casing_options') {
                continue;
            }
            if ($v === null || $v === '' || (is_array($v) && empty($v))) {
                continue;
            }
            if (is_array($v) && isset($v[0]) && is_array($v[0])) {
                continue;
            }
            if (is_array($v)) {
                $display = implode(', ', array_map(function ($x) {
                    return humanize_token((string) $x);
                }, $v));
            } else {
                $display = humanize_token((string) $v);
            }
            $push($k, humanize_token($k), $display);
        }

        return $segments;
    }
}

if (!function_exists('admin_get_quote_description_overrides')) {
    function admin_get_quote_description_overrides(array $options) {
        $raw = $options['_admin_quote_description_overrides'] ?? [];
        if (!is_array($raw)) {
            return [];
        }
        $out = [];
        foreach ($raw as $id => $entry) {
            if (!is_array($entry)) {
                continue;
            }
            $segId = admin_quote_description_segment_id((string) $id);
            $base = trim((string) ($entry['base'] ?? ''));
            $text = trim((string) ($entry['text'] ?? ''));
            if ($segId === '' || $text === '') {
                continue;
            }
            $out[$segId] = ['base' => $base, 'text' => $text];
        }
        return $out;
    }
}

if (!function_exists('admin_apply_quote_description_overrides')) {
    function admin_apply_quote_description_overrides(array $segments, array $overrides) {
        foreach ($segments as $i => $segment) {
            $id = (string) ($segment['id'] ?? '');
            if ($id === '' || !isset($overrides[$id])) {
                continue;
            }
            $baseDisplay = (string) ($segment['display_value'] ?? '');
            $override = $overrides[$id];
            if ((string) ($override['base'] ?? '') === $baseDisplay) {
                $segments[$i]['display_value'] = (string) ($override['text'] ?? $baseDisplay);
            }
        }
        return $segments;
    }
}

if (!function_exists('admin_quote_description_attribute_key')) {
    function admin_quote_description_attribute_key() {
        return 'quote_description';
    }
}

if (!function_exists('admin_build_quote_description_line_item_attribute')) {
    /**
     * Single Shopify line item attribute for PandaDoc / integrations (values only, slash-separated).
     *
     * @return array{key: string, value: string}|null
     */
    function admin_build_quote_description_line_item_attribute(array $options) {
        $text = admin_build_options_values_only_text($options);
        if ($text === '') {
            return null;
        }
        return ['key' => admin_quote_description_attribute_key(), 'value' => $text];
    }
}

if (!function_exists('admin_build_configuration_quote_description_html')) {
    /**
     * Slash-separated option values for quote preview, PDF, and HubSpot (no labels).
     */
    function admin_build_configuration_quote_description_html(array $options, $forPdf = false) {
        $text = admin_build_options_values_only_text($options);
        if ($text === '') {
            return '';
        }
        return admin_quote_fill_template(admin_quote_template_pdf_description(), [
            'margin_top' => $forPdf ? '4px' : '0.35rem',
            'font_size'  => $forPdf ? '8pt' : '0.85rem',
            'text'       => admin_quote_h($text),
        ]);
    }
}

if (!function_exists('admin_build_options_values_only_text')) {
    /** Option values only (no labels), for configuration quote preview and PDF. */
    function admin_build_options_values_only_text(array $options, $separator = ' / ') {
        $segments = admin_build_quote_description_segments($options);
        $segments = admin_apply_quote_description_overrides($segments, admin_get_quote_description_overrides($options));
        $values = [];
        foreach ($segments as $segment) {
            $value = trim((string) ($segment['display_value'] ?? ''));
            if ($value !== '') {
                $values[] = $value;
            }
        }
        return implode($separator, $values);
    }
}

if (!function_exists('admin_config_door_product_type')) {
    /** Normalized product type string for the saved configuration. */
    function admin_config_door_product_type(array $options, array $config = []) {
        return strtolower(trim((string) ($options['_product_type'] ?? ($config['product_type'] ?? ''))));
    }
}

if (!function_exists('admin_config_is_interior_door_product')) {
    /** True when the product type is "Interior Doors". */
    function admin_config_is_interior_door_product(array $options, array $config = []) {
        $type = admin_config_door_product_type($options, $config);
        return $type !== '' && strpos($type, 'interior door') !== false;
    }
}

if (!function_exists('admin_config_is_exterior_door_product')) {
    /** True when the product type is "Exterior Doors". */
    function admin_config_is_exterior_door_product(array $options, array $config = []) {
        $type = admin_config_door_product_type($options, $config);
        return $type !== '' && strpos($type, 'exterior door') !== false;
    }
}

if (!function_exists('admin_config_is_door_quote_product')) {
    /** True for any door product that uses the generated single-line quote description. */
    function admin_config_is_door_quote_product(array $options, array $config = []) {
        return admin_config_is_interior_door_product($options, $config)
            || admin_config_is_exterior_door_product($options, $config);
    }
}

if (!function_exists('admin_build_interior_door_description')) {
    /**
     * Human-readable description for Interior / Exterior Doors, built from saved configuration options.
     * Returns '' for any other product type or when there is no data.
     * Single line, segments joined by " / ". The two product types use different
     * field sequences (see the $isExterior branch below).
     *
     * Interior example:
     *   Exact Door Size: 26w / 98h / 2 1/4t Premium Thickness Upgrade Required Due To Oversized Height /
     *   Double Door with S-12 Sidelight On Right Only and T-10 Transom Above / Exact Sidelight Width: 12w /
     *   Exact Transom Height: 12h / Pre-Hung on Matching Jamb: 18 9/16w / Black Hinges / Double Bore /
     *   S3M Shelf / Raised Beaded Grooved Panel / Double In Swing-Active Door on Left from Outside View / ...
     *
     * Exterior example:
     *   Black Walnut / Removable Inserts / Black Aluminum / Energy Efficient Low E Glass Insert /
     *   Bright White Frame / Single Door with SSL-08 Sidelight On Right Only and ST-05 Transom Above /
     *   Exact Sidelight Width: 21 5/16w / Exact Transom Height: 30h /
     *   Pre-Hung Unit Size: 44 7/16w / 80h / 2 1/4t Premium Thickness Upgrade Required Due To Oversized Width /
     *   Pre-Hung on Matching Jamb: 12 5/8w / Oil Rubbed Bronze Hinges / Door Sweep /
     *   African Mahogany Door Stops with Seals / Primed White / Pet Door Size: Large 12w x 16h / Out Left
     */
    function admin_build_interior_door_description(array $options, array $config = []) {
        if (!admin_config_is_door_quote_product($options, $config)) {
            return '';
        }
        $isExterior = admin_config_is_exterior_door_product($options, $config);

        $schema = (isset($options['_pricing_schema']) && is_array($options['_pricing_schema'])) ? $options['_pricing_schema'] : [];
        $byId = [];
        foreach ($schema as $o) {
            if (is_array($o) && !empty($o['id'])) {
                $byId[admin_norm_option_id($o['id'])] = $o;
            }
        }

        $humanize = function ($v) {
            $v = trim((string) $v);
            if ($v === '') {
                return '';
            }
            return ucwords(str_replace(['_', '-'], ' ', $v));
        };

        // Selected option -> display label (first id with a value wins).
        $label = function ($ids) use ($options, $byId, $humanize) {
            foreach ((array) $ids as $id) {
                $val = admin_config_get_value($options, $id);
                if ($val === null || $val === '' || (is_array($val) && !$val)) {
                    continue;
                }
                $opt = $byId[admin_norm_option_id($id)] ?? null;
                if (is_array($val)) {
                    $parts = [];
                    foreach ($val as $vv) {
                        $parts[] = $opt ? admin_schema_choice_label($opt, $vv) : $humanize($vv);
                    }
                    $parts = array_values(array_filter(array_map('trim', $parts), function ($s) { return $s !== ''; }));
                    if ($parts) {
                        return implode(', ', $parts);
                    }
                    continue;
                }
                return $opt ? admin_schema_choice_label($opt, $val) : $humanize($val);
            }
            return '';
        };

        // int + fraction -> "36" or "36 1/2".
        $dim = function ($intIds, $fracIds) use ($options) {
            $int = '';
            foreach ((array) $intIds as $k) {
                $v = admin_config_get_value($options, $k);
                if ($v !== null && trim((string) $v) !== '') { $int = trim((string) $v); break; }
            }
            $frac = '';
            foreach ((array) $fracIds as $k) {
                $v = admin_config_get_value($options, $k);
                if ($v !== null && trim((string) $v) !== '') { $frac = trim((string) $v); break; }
            }
            if (in_array($frac, ['0', '0/0', '0"', '0 '], true)) { $frac = ''; }
            if ($int === '' && $frac === '') { return ''; }
            if ($int !== '' && $frac !== '') { return $int . ' ' . $frac; }
            return $int !== '' ? $int : $frac;
        };

        $truthy = function ($ids) use ($options) {
            foreach ((array) $ids as $id) {
                $v = admin_config_get_value($options, $id);
                $vals = is_array($v) ? $v : [$v];
                foreach ($vals as $x) {
                    if (in_array(strtolower(trim((string) $x)), ['true', '1', 'yes', 'on'], true)) {
                        return true;
                    }
                }
            }
            return false;
        };

        $parts = [];

        $breakdown = (isset($options['_addon_breakdown']) && is_array($options['_addon_breakdown'])) ? $options['_addon_breakdown'] : [];
        $oversized = ((float) ($breakdown['int_ext_oversized'] ?? 0) > 0) || ((float) ($breakdown['screen_storm_oversized'] ?? 0) > 0);

        // --- Measurement tokens: "Exact Door Size: 26w / 98h / 2 1/4t [premium]" ---
        $measurementType = $label(['measurement_type']);
        $width = $dim(['exact_door_width_int', 'exact_door_width', 'door_width'], ['exact_door_width_frac', 'door_width_fraction']);
        $height = $dim(['door_height', 'exact_door_height', 'exact_door_height_int'], ['door_height_fraction', 'door_height_frac', 'exact_door_height_frac']);
        $thickness = '';
        foreach (['exact_door_thickness_frac', 'exact_door_thickness', 'door_thickness'] as $k) {
            $v = admin_config_get_value($options, $k);
            if ($v !== null && trim((string) $v) !== '') { $thickness = trim((string) $v); break; }
        }

        $measTokens = [];
        if ($width !== '') { $measTokens[] = $width . 'w'; }
        if ($height !== '') { $measTokens[] = $height . 'h'; }
        $thicknessTok = $thickness !== '' ? $thickness . 't' : '';
        if ($oversized) {
            // No dedicated width/height oversized flag exists; infer from the height
            // (standard door height tops out around 96"). Tall units -> "Height".
            $oversizedDim = ((int) $height) > 96 ? 'Height' : 'Width';
            $premium = 'Premium Thickness Upgrade Required Due To Oversized ' . $oversizedDim;
            $thicknessTok = $thicknessTok !== '' ? ($thicknessTok . ' ' . $premium) : $premium;
        }
        if ($thicknessTok !== '') { $measTokens[] = $thicknessTok; }
        if ($measTokens && $measurementType !== '') {
            $measTokens[0] = $measurementType . ': ' . $measTokens[0];
        }
        if (!$measTokens && $measurementType !== '') {
            $measTokens[] = $measurementType;
        }

        // --- Combined door + sidelight + transom ---
        $sidelightStyle = $label(['sidelight_style']);
        $sidelightLocation = $label(['sidelight_location']);
        $transomStyle = $label(['transom_style']);
        $hasSidelight = $sidelightStyle !== '' || $truthy(['sidelight_design']);
        $hasTransom = $transomStyle !== '' || $truthy(['transom_design']);

        $comboSeg = '';
        if ($hasSidelight || $hasTransom) {
            $doorType = $label(['door_unit_design', 'door_type', 'panel_unit_design']);
            $comboSeg = $doorType !== '' ? $doorType : 'Door';
            $withBits = [];
            if ($hasSidelight && $sidelightStyle !== '') {
                $sb = $sidelightStyle . ' Sidelight';
                if ($sidelightLocation !== '') { $sb .= ' On ' . $sidelightLocation; }
                $withBits[] = $sb;
            }
            if ($hasTransom && $transomStyle !== '') {
                $withBits[] = $transomStyle . ' Transom Above';
            }
            if ($withBits) { $comboSeg .= ' with ' . implode(' and ', $withBits); }
        }

        // --- Exact sidelight width / transom height ---
        $slw = $dim(['sidelight_width', 'sidelight_width_int'], ['sidelight_width_fraction', 'sidelight_width_frac']);
        $slwSeg = ($hasSidelight && $slw !== '') ? ('Exact Sidelight Width: ' . $slw . 'w') : '';
        $th = $dim(['panel_transom_height_int', 'panel_transom_height'], ['panel_transom_height_frac', 'panel_transom_height_fraction']);
        $thSeg = ($hasTransom && $th !== '') ? ('Exact Transom Height: ' . $th . 'h') : '';

        // --- Pre-hung (+ jamb width) ---
        $preHung = $label(['pre_hung']);
        $preHungSeg = '';
        if ($preHung !== '') {
            $jamb = $dim(['jamb_pre_hung_int', 'panel_pre_hung_int', 'rough_pre_hung_int'], ['jamb_pre_hung_frac', 'panel_pre_hung_frac']);
            if ($jamb === '') {
                $v = admin_config_get_value($options, 'exact_pre_hung_frac');
                if ($v !== null && trim((string) $v) !== '') { $jamb = trim((string) $v); }
            }
            $preHungSeg = $jamb !== '' ? ($preHung . ': ' . $jamb . 'w') : $preHung;
        }

        $hinge = $label(['hinge_finish']);
        $swing = $label(['swing_direction']);

        if ($isExterior) {
            // --- Exterior Doors sequence ---
            $woodSpecie  = $label(['wood_type', 'wood_type_storm_porch']);
            if ($woodSpecie !== '') { $parts[] = $woodSpecie; }
            $screenStormFn = $label(['screen_and_storm_inserts']);
            if ($screenStormFn !== '') { $parts[] = $screenStormFn; }
            $screenType = $label(['screen_type']);
            if ($screenType !== '') { $parts[] = $screenType; }
            $stormType = $label(['storm_glass_type', 'storm_type']);
            if ($stormType !== '') { $parts[] = $stormType; }
            $frameColour = $label(['screen_storm_insert_frame_color', 'screen_storm_frame_color']);
            if ($frameColour !== '') { $parts[] = $frameColour; }

            if ($comboSeg !== '') { $parts[] = $comboSeg; }
            if ($slwSeg !== '') { $parts[] = $slwSeg; }
            if ($thSeg !== '') { $parts[] = $thSeg; }

            foreach ($measTokens as $mt) { $parts[] = $mt; }
            if ($preHungSeg !== '') { $parts[] = $preHungSeg; }

            if ($hinge !== '') { $parts[] = $hinge . ' Hinges'; }
            $sweep = $label(['door_sweep']);
            if ($sweep !== '') { $parts[] = $sweep; }
            $sealKit = $label(['door_seal_kit']);
            if ($sealKit !== '') { $parts[] = $sealKit; }
            $priming = $label(['priming']);
            if ($priming !== '') { $parts[] = $priming; }
            $petDoor = $label(['pet_door']);
            if ($petDoor !== '') {
                $parts[] = (stripos($petDoor, 'pet door') !== false) ? $petDoor : ('Pet Door Size: ' . $petDoor);
            }
            if ($swing !== '') { $parts[] = $swing; }
        } else {
            // --- Interior Doors sequence ---
            $glass = $label(['glass_type', 'storm_glass_type']);
            $wood = $label(['wood_type', 'wood_type_storm_porch']);
            $exposure = $label(['door_location_exposure']);
            $material = implode(' / ', array_values(array_filter([$glass, $wood], function ($s) { return trim((string) $s) !== ''; })));
            if ($exposure !== '') {
                $material = ($material !== '' ? $material . ' ' : '') . '(' . $exposure . ')';
            }
            if ($material !== '') { $parts[] = $material; }

            foreach ($measTokens as $mt) { $parts[] = $mt; }
            if ($comboSeg !== '') { $parts[] = $comboSeg; }
            if ($slwSeg !== '') { $parts[] = $slwSeg; }
            if ($thSeg !== '') { $parts[] = $thSeg; }
            if ($preHungSeg !== '') { $parts[] = $preHungSeg; }

            if ($hinge !== '') { $parts[] = $hinge . ' Hinges'; }
            $lockset = $label(['lockset_prep']);
            if ($lockset !== '') { $parts[] = $lockset; }
            $shelf = $label(['shelf']);
            if ($shelf !== '') { $parts[] = (stripos($shelf, 'shelf') !== false) ? $shelf : ($shelf . ' Shelf'); }
            $panel = $label(['panel_profile', 'stile_and_rail_profile']);
            if ($panel !== '') { $parts[] = $panel; }
            if ($swing !== '') { $parts[] = $swing; }

            // --- Additional options (e.g. threshold, brick molding, door bolt) ---
            foreach (['additional_options', 'additional_addons', 'extras', 'hardware_options'] as $addId) {
                $addOpt = $byId[admin_norm_option_id($addId)] ?? null;
                $addVal = admin_config_get_value($options, $addId);
                if ($addVal === null || $addVal === '' || (is_array($addVal) && !$addVal)) {
                    continue;
                }
                $vals = is_array($addVal) ? $addVal : [$addVal];
                foreach ($vals as $vv) {
                    $lab = trim((string) ($addOpt ? admin_schema_choice_label($addOpt, $vv) : $humanize($vv)));
                    if ($lab !== '') { $parts[] = $lab; }
                }
            }
        }

        // --- Optional admin/staff free text ---
        $extra = '';
        foreach (['_admin_door_description_note', '_admin_quote_extra_text'] as $k) {
            if (isset($options[$k]) && trim((string) $options[$k]) !== '') { $extra = trim((string) $options[$k]); break; }
        }
        if ($extra === '') {
            $notes = trim((string) ($config['notes_sales'] ?? ''));
            if ($notes !== '') { $extra = $notes; }
        }
        if ($extra !== '') {
            $parts[] = $extra;
        }

        return implode(' / ', $parts);
    }
}

if (!function_exists('admin_get_quote_description_text')) {
    /**
     * Description used for the configuration quote preview + quote email.
     * Interior / Exterior Doors get the generated description (or the staff-edited
     * verbatim text if saved); all other products keep the slash-separated summary.
     */
    function admin_get_quote_description_text(array $options, array $config = []) {
        if (admin_config_is_door_quote_product($options, $config)) {
            $saved = trim((string) ($options['_admin_quote_description_text'] ?? ''));
            if ($saved !== '') {
                return $saved;
            }
            $generated = admin_build_interior_door_description($options, $config);
            if ($generated !== '') {
                return $generated;
            }
        }
        return admin_build_options_values_only_text($options);
    }
}

if (!function_exists('admin_get_configuration_quote_title')) {
    function admin_get_configuration_quote_title(array $options, array $config = []) {
        $override = trim((string) ($options['_admin_quote_title'] ?? ''));
        if ($override !== '') {
            return $override;
        }
        $title = trim((string) ($options['_product_title'] ?? ($config['product_title'] ?? '')));
        return $title !== '' ? $title : 'Door Configuration';
    }
}

if (!function_exists('admin_save_draft_order_title_from_post')) {
    function admin_save_draft_order_title_from_post(array $options, array $post) {
        if (!empty($post['admin_use_custom_draft_title'])) {
            $options['_admin_use_custom_draft_title'] = true;
            $title = trim((string) ($post['admin_draft_order_title'] ?? ''));
            if ($title !== '') {
                $options['_admin_quote_title'] = $title;
            } else {
                unset($options['_admin_quote_title']);
            }
        } else {
            unset($options['_admin_use_custom_draft_title'], $options['_admin_quote_title']);
        }
        return $options;
    }
}

if (!function_exists('admin_get_draft_order_line_item_title')) {
    /**
     * Shopify draft order line item title.
     * When staff enables custom title, use it as-is; otherwise prefix product title with "Custom ".
     */
    function admin_get_draft_order_line_item_title(array $options, array $config = []) {
        if (!empty($options['_admin_use_custom_draft_title'])) {
            $custom = trim((string) ($options['_admin_quote_title'] ?? ''));
            if ($custom !== '') {
                return $custom;
            }
        }
        $default = trim((string) ($options['_product_title'] ?? ($config['product_title'] ?? '')));
        $configId = (int) ($config['id'] ?? 0);
        $title = $default !== '' ? $default : ('Door Configuration' . ($configId > 0 ? ' #' . $configId : ''));
        if (stripos($title, 'custom ') !== 0) {
            $title = 'Custom ' . $title;
        }
        return $title;
    }
}

if (!function_exists('admin_merge_quote_description_overrides_from_edit')) {
    /**
     * Compare edited slash-separated text to generated segments; store per-segment display overrides.
     * Does not change real configuration option values on the detail page.
     */
    function admin_merge_quote_description_overrides_from_edit(array $segments, $editedText, array $existingOverrides = []) {
        $parts = preg_split('/\s*\/\s*/', trim((string) $editedText)) ?: [];
        $parts = array_values(array_map('trim', $parts));
        $overrides = $existingOverrides;

        foreach ($segments as $i => $segment) {
            if (!empty($segment['__measurement_block__'])) {
                continue;
            }
            $id = (string) ($segment['id'] ?? '');
            if ($id === '') {
                continue;
            }
            $generated = trim((string) ($segment['display_value'] ?? ''));
            $edited = trim((string) ($parts[$i] ?? ''));
            if ($edited === '' || $edited === $generated) {
                unset($overrides[$id]);
                continue;
            }
            $overrides[$id] = [
                'base' => $generated,
                'text' => $edited,
            ];
        }

        return $overrides;
    }
}

if (!function_exists('admin_save_configuration_quote_preview')) {
    /**
     * Persist staff-edited quote title + description overrides and current config options.
     *
     * @return array{success: bool, error: string}
     */
    function admin_save_configuration_quote_preview(PDO $pdo, array $config, array $post) {
        $configId = (int) ($config['id'] ?? 0);
        if ($configId <= 0) {
            return ['success' => false, 'error' => 'Invalid configuration.'];
        }

        $options = json_decode((string) ($config['options_json'] ?? ''), true);
        if (!is_array($options)) {
            $options = [];
        }

        $built = admin_configuration_options_from_form_post($options, $config, $post);
        $options = $built['options'];
        $basePrice = $built['base_price'];
        $computedPrice = $built['computed_price'];

        $editedDescription = trim((string) ($post['staff_quote_description'] ?? ''));
        if (admin_config_is_door_quote_product($options, $config)) {
            // Interior / Exterior Doors use a generated single-line description; store the
            // staff-edited text verbatim (do not map it onto slash-separated segment overrides).
            $generatedDescription = admin_build_interior_door_description($options, $config);
            if ($editedDescription === '' || $editedDescription === $generatedDescription) {
                unset($options['_admin_quote_description_text']);
            } else {
                $options['_admin_quote_description_text'] = $editedDescription;
            }
        } else {
            $segments = admin_build_quote_description_segments($options);
            $existingOverrides = admin_get_quote_description_overrides($options);
            $options['_admin_quote_description_overrides'] = admin_merge_quote_description_overrides_from_edit(
                $segments,
                $editedDescription,
                $existingOverrides
            );
            if (empty($options['_admin_quote_description_overrides'])) {
                unset($options['_admin_quote_description_overrides']);
            }
        }

        $staffTitle = trim((string) ($post['staff_quote_title'] ?? ''));
        $defaultTitle = trim((string) ($options['_product_title'] ?? ($config['product_title'] ?? '')));
        if ($staffTitle !== '' && $staffTitle !== $defaultTitle) {
            $options['_admin_quote_title'] = $staffTitle;
        } else {
            unset($options['_admin_quote_title']);
        }

        $status = (string) ($config['status'] ?? 'draft');
        $notesSales = (string) ($config['notes_sales'] ?? '');

        $up = $pdo->prepare('UPDATE door_configurations SET options_json=?, base_price=?, computed_price=?, notes_sales=? WHERE id=?');
        $up->execute([json_encode($options), $basePrice, $computedPrice, $notesSales, $configId]);

        return ['success' => true, 'error' => ''];
    }
}

if (!function_exists('admin_build_options_summary')) {
    function admin_build_options_summary(array $options) {
        $parts = [];
        foreach (admin_build_options_summary_lines($options) as $line) {
            $parts[] = $line['label'] . ': ' . $line['value'];
        }
        return implode('; ', $parts);
    }
}

if (!function_exists('admin_build_line_item_properties_from_options')) {
    function admin_build_line_item_properties_from_options(array $options, int $configId = 0) {
        $props = [];
        foreach (admin_build_options_summary_lines($options) as $line) {
            $label = trim((string)($line['label'] ?? ''));
            $value = trim((string)($line['value'] ?? ''));
            if ($label === '' || $value === '') continue;
            if (strcasecmp($label, 'Quantity') === 0) continue;
            $props[$label] = $value;
        }
        if ($configId > 0) {
            $props['Config ID'] = (string)$configId;
        }
        $customUrl = trim((string) ($options['_custom_image_url'] ?? ''));
        if ($customUrl !== '') {
            $props['custom_image_url'] = $customUrl;
        }
        return $props;
    }
}

if (!function_exists('admin_merge_line_item_property_overrides')) {
    function admin_merge_line_item_property_overrides(array $baseProps, array $overrideProps) {
        if (empty($overrideProps)) return $baseProps;
        $merged = $baseProps;
        $normIndex = [];
        foreach ($merged as $k => $v) {
            $normIndex[admin_norm_property_key($k)] = $k;
        }
        foreach ($overrideProps as $ok => $ov) {
            $nk = admin_norm_property_key($ok);
            if (isset($normIndex[$nk])) {
                $merged[$normIndex[$nk]] = (string)$ov;
            } else {
                $merged[(string)$ok] = (string)$ov;
                $normIndex[$nk] = (string)$ok;
            }
        }
        return $merged;
    }
}

if (!function_exists('admin_get_merged_line_item_properties')) {
    function admin_get_merged_line_item_properties(array $options, int $configId = 0) {
        $base = admin_build_line_item_properties_from_options($options, $configId);
        $overrideText = trim((string)($options['_admin_line_item_properties_override'] ?? ''));
        if ($overrideText === '') return $base;
        return admin_merge_line_item_property_overrides($base, admin_parse_line_item_properties_override($overrideText));
    }
}

if (!function_exists('admin_line_item_properties_for_shopify')) {
    function admin_line_item_properties_for_shopify(array $props) {
        $out = [];
        foreach ($props as $name => $value) {
            if ($name === '' || $value === null || (string)$value === '') continue;
            $out[] = ['name' => (string)$name, 'value' => (string)$value];
        }
        return $out;
    }
}

if (!function_exists('admin_normalize_internal_image_urls')) {
    function admin_normalize_internal_image_urls($value) {
        if (is_array($value)) {
            return array_values(array_filter(array_map(function ($url) {
                return trim((string) $url);
            }, $value)));
        }
        if (is_string($value) && trim($value) !== '') {
            return array_values(array_filter(array_map('trim', explode(',', $value))));
        }
        return [];
    }
}

if (!function_exists('admin_build_config_image_line_item_attributes')) {
    /**
     * Shopify line item customAttributes for customer-facing config image only.
     * Internal images (_internal_images) are stored on the configuration but not sent to draft orders or PandaDoc.
     */
    function admin_build_config_image_line_item_attributes(array $options) {
        $attrs = [];
        $customUrl = trim((string) ($options['_custom_image_url'] ?? ''));
        if ($customUrl !== '') {
            $attrs[] = ['key' => 'custom_image_url', 'value' => $customUrl];
        }
        return $attrs;
    }
}

if (!function_exists('admin_append_config_image_line_item_attributes')) {
    function admin_append_config_image_line_item_attributes(array $customAttributes, array $options) {
        return array_merge($customAttributes, admin_build_config_image_line_item_attributes($options));
    }
}

if (!function_exists('admin_shopify_upload_staged_file')) {
    /**
     * Upload a file to Shopify Files via staged upload + fileCreate.
     *
     * @param string $resource   StagedUploadInput resource (IMAGE|FILE)
     * @param string $contentType FileCreateInput contentType (IMAGE|FILE)
     * @param string $pollFragment GraphQL inline fragment for polling (MediaImage|GenericFile)
     */
    function admin_shopify_upload_staged_file($tmpPath, $filename, $mimeType, $resource, $contentType, $pollFragment) {
        if (!admin_shopify_is_configured()) {
            return ['success' => false, 'url' => '', 'error' => 'Shopify API not configured'];
        }
        if (!is_file($tmpPath) || !is_readable($tmpPath)) {
            return ['success' => false, 'url' => '', 'error' => 'Upload file not readable'];
        }
        $filename = trim((string) $filename) !== '' ? (string) $filename : basename((string) $tmpPath);
        $mimeType = trim((string) $mimeType) !== '' ? (string) $mimeType : 'application/octet-stream';

        $stageMutation = <<<'GQL'
mutation adminStagedUploadCreate($input: [StagedUploadInput!]!) {
  stagedUploadsCreate(input: $input) {
    stagedTargets { url resourceUrl parameters { name value } }
    userErrors { field message }
  }
}
GQL;
        $stage = admin_shopify_gql_request($stageMutation, [
            'input' => [[
                'resource'   => (string) $resource,
                'filename'   => $filename,
                'mimeType'   => $mimeType,
                'httpMethod' => 'POST',
            ]],
        ]);
        if (!$stage['ok']) {
            return ['success' => false, 'url' => '', 'error' => $stage['error']];
        }
        $target = $stage['data']['stagedUploadsCreate']['stagedTargets'][0] ?? null;
        $stageErrors = $stage['data']['stagedUploadsCreate']['userErrors'] ?? [];
        if (!is_array($target) || empty($target['url']) || !empty($stageErrors)) {
            $msg = !empty($stageErrors)
                ? implode('; ', array_column($stageErrors, 'message'))
                : 'Shopify staged upload failed';
            return ['success' => false, 'url' => '', 'error' => $msg];
        }

        $postFields = [];
        foreach ($target['parameters'] ?? [] as $param) {
            if (!is_array($param) || empty($param['name'])) {
                continue;
            }
            $postFields[(string) $param['name']] = (string) ($param['value'] ?? '');
        }
        if (class_exists('CURLFile')) {
            $postFields['file'] = new CURLFile($tmpPath, $mimeType, $filename);
        } else {
            $postFields['file'] = '@' . $tmpPath;
        }
        $uploadCh = curl_init((string) $target['url']);
        curl_setopt_array($uploadCh, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $postFields,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 120,
        ]);
        $uploadBody = curl_exec($uploadCh);
        $uploadErr = curl_error($uploadCh);
        $uploadCode = (int) curl_getinfo($uploadCh, CURLINFO_HTTP_CODE);
        curl_close($uploadCh);
        if ($uploadErr || ($uploadCode < 200 || $uploadCode >= 300)) {
            return [
                'success' => false,
                'url'     => '',
                'error'   => $uploadErr ?: ('Shopify staged upload HTTP ' . $uploadCode . ($uploadBody ? ': ' . substr((string) $uploadBody, 0, 200) : '')),
            ];
        }

        $createMutation = <<<'GQL'
mutation adminFileCreate($files: [FileCreateInput!]!) {
  fileCreate(files: $files) {
    files {
      id
      fileStatus
      __POLL_FRAGMENT__
    }
    userErrors { field message }
  }
}
GQL;
        $createMutation = str_replace('__POLL_FRAGMENT__', (string) $pollFragment, $createMutation);
        $create = admin_shopify_gql_request($createMutation, [
            'files' => [[
                'alt'            => $filename,
                'contentType'    => (string) $contentType,
                'originalSource' => (string) ($target['resourceUrl'] ?? ''),
            ]],
        ]);
        if (!$create['ok']) {
            return ['success' => false, 'url' => '', 'error' => $create['error']];
        }
        $createErrors = $create['data']['fileCreate']['userErrors'] ?? [];
        if (!empty($createErrors)) {
            return [
                'success' => false,
                'url'     => '',
                'error'   => implode('; ', array_column($createErrors, 'message')),
            ];
        }
        $fileNode = $create['data']['fileCreate']['files'][0] ?? null;
        if (!is_array($fileNode) || empty($fileNode['id'])) {
            return ['success' => false, 'url' => '', 'error' => 'Shopify file create returned no file'];
        }

        $fileGid = (string) $fileNode['id'];
        $fileUrl = trim((string) ($fileNode['url'] ?? ($fileNode['image']['url'] ?? '')));
        $status = (string) ($fileNode['fileStatus'] ?? '');
        if ($fileUrl === '' || $status === 'PROCESSING' || $status === 'UPLOADED') {
            $pollQuery = <<<'GQL'
query adminPollFile($id: ID!) {
  node(id: $id) {
    __POLL_FRAGMENT__
  }
}
GQL;
            $pollQuery = str_replace('__POLL_FRAGMENT__', (string) $pollFragment, $pollQuery);
            for ($i = 0; $i < 12 && $fileUrl === ''; $i++) {
                if ($i > 0) {
                    usleep(500000);
                }
                $poll = admin_shopify_gql_request($pollQuery, ['id' => $fileGid]);
                if (!$poll['ok']) {
                    continue;
                }
                $node = $poll['data']['node'] ?? null;
                if (!is_array($node)) {
                    continue;
                }
                $fileUrl = trim((string) ($node['url'] ?? ($node['image']['url'] ?? '')));
                $status = (string) ($node['fileStatus'] ?? $status);
                if ($status === 'FAILED') {
                    return ['success' => false, 'url' => '', 'error' => 'Shopify file processing failed'];
                }
            }
        }
        if ($fileUrl === '') {
            return ['success' => false, 'url' => '', 'error' => 'Shopify file URL not ready yet. Try again in a moment.'];
        }
        return ['success' => true, 'url' => $fileUrl, 'error' => ''];
    }
}

if (!function_exists('admin_shopify_upload_pdf_file')) {
    function admin_shopify_upload_pdf_file($tmpPath, $filename) {
        $mediaFragment = <<<'GQL'
... on MediaImage {
  fileStatus
  image { url }
}
GQL;
        $genericFragment = <<<'GQL'
... on GenericFile {
  fileStatus
  url
}
GQL;
        $result = admin_shopify_upload_staged_file(
            $tmpPath,
            $filename,
            'application/pdf',
            'FILE',
            'FILE',
            $genericFragment
        );
        if (!empty($result['success'])) {
            return $result;
        }
        return admin_shopify_upload_staged_file(
            $tmpPath,
            $filename,
            'application/pdf',
            'FILE',
            'FILE',
            $mediaFragment
        );
    }
}

if (!function_exists('admin_shopify_upload_image_file')) {
    function admin_shopify_upload_image_file($tmpPath, $filename, $mimeType) {
        $fragment = <<<'GQL'
... on MediaImage {
  fileStatus
  image { url }
}
GQL;
        return admin_shopify_upload_staged_file($tmpPath, $filename, $mimeType, 'IMAGE', 'IMAGE', $fragment);
    }
}

if (!function_exists('admin_resolve_config_variant_id')) {
    function admin_resolve_config_variant_id(array $config, array $options) {
        foreach (['_variant_id', 'variant_id', 'variantId'] as $key) {
            if (!empty($options[$key])) return preg_replace('/\D/', '', (string)$options[$key]);
        }
        foreach (['variant_id', 'product_variant_id'] as $key) {
            if (!empty($config[$key])) return preg_replace('/\D/', '', (string)$config[$key]);
        }
        return '';
    }
}

if (!function_exists('admin_option_has_slab_choice')) {
    function admin_option_has_slab_choice(array $opt) {
        foreach ($opt['options'] ?? [] as $c) {
            if (!is_array($c)) continue;
            $v = strtolower(str_replace('-', '_', (string)($c['value'] ?? '')));
            if ($v === 'slab_only') return true;
        }
        return false;
    }
}

if (!function_exists('admin_accordion_group_for_option')) {
    function admin_accordion_group_for_option(array $opt) {
        if (admin_option_has_slab_choice($opt)) {
            return 'door_design';
        }
        if (($opt['_adminGroup'] ?? '') === 'measurement') {
            return 'measurements';
        }
        $id = strtolower(str_replace('-', '_', (string)($opt['id'] ?? '')));
        if ($id === 'hardware_options') {
            return 'hardware';
        }
        if ($id === 'casing_options') {
            return 'casing';
        }
        if ($id === 'hinge_finish') {
            return 'other';
        }
        if (strpos($id, 'sidelight') !== false || strpos($id, 'transom') !== false) {
            return 'sidelight_transom';
        }
        if ($id === 'measurement_type' || preg_match('/(width|height|thick|pre_hung|jamb)/', $id)) {
            return 'measurements';
        }
        if (in_array($id, ['wood_type', 'wood_type_storm_porch', 'door_location_exposure'], true)) {
            return 'wood_materials';
        }
        if (preg_match('/(glass|panel|storm|insert|bevel|molding|stile|rail|edge|screen)/', $id)) {
            return 'glass_panel';
        }
        if (preg_match('/(door_unit|top_style|pre_hung|slab|moulding)/', $id)) {
            return 'door_design';
        }
        return 'other';
    }
}

if (!function_exists('admin_accordion_group_labels')) {
    function admin_accordion_group_labels() {
        return [
            'door_design' => 'Door design',
            'sidelight_transom' => 'Sidelight & transom',
            'wood_materials' => 'Wood & location',
            'glass_panel' => 'Glass & panel',
            'measurements' => 'Measurements',
            'hardware' => 'Hardware',
            'casing' => 'Casing',
            'other' => 'Additional options',
        ];
    }
}

if (!function_exists('admin_bucket_schema_by_accordion')) {
    function admin_bucket_schema_by_accordion(array $schema) {
        $buckets = [];
        foreach (array_keys(admin_accordion_group_labels()) as $key) {
            $buckets[$key] = [];
        }
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id'])) continue;
            $group = admin_accordion_group_for_option($opt);
            if (!isset($buckets[$group])) {
                $buckets[$group] = [];
            }
            $buckets[$group][] = $opt;
        }
        return $buckets;
    }
}

if (!function_exists('admin_normalize_web_path')) {
    function admin_normalize_web_path($path) {
        $path = str_replace('\\', '/', (string) $path);
        if ($path === '/' || $path === '.') {
            return '';
        }
        return rtrim($path, '/');
    }
}

if (!function_exists('admin_current_web_base')) {
    function admin_current_web_base() {
        if (isset($GLOBALS['admin_web_base']) && $GLOBALS['admin_web_base'] !== '') {
            return admin_normalize_web_path($GLOBALS['admin_web_base']);
        }
        return admin_normalize_web_path(dirname($_SERVER['SCRIPT_NAME'] ?? ''));
    }
}

if (!function_exists('admin_page_url')) {
    function admin_page_url($page, array $query = [], $basePath = null) {
        if (defined('ADMIN_BASE_URL') && ADMIN_BASE_URL !== '') {
            $url = rtrim(ADMIN_BASE_URL, '/') . '/' . ltrim((string) $page, '/');
        } else {
            $base = $basePath !== null ? admin_normalize_web_path($basePath) : admin_current_web_base();
            $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
            $host = $_SERVER['HTTP_HOST'] ?? 'localhost';
            $url = $scheme . '://' . $host . ($base !== '' ? $base : '') . '/' . ltrim((string) $page, '/');
        }
        if (!empty($query)) {
            $url .= (strpos($url, '?') !== false ? '&' : '?') . http_build_query($query);
        }
        return $url;
    }
}

if (!function_exists('admin_contact_save_url')) {
    function admin_contact_save_url($basePath = null) {
        return admin_page_url('folder_contact_save.php', [], $basePath);
    }
}

if (!function_exists('admin_configuration_remove_url')) {
    function admin_configuration_remove_url($basePath = null) {
        return admin_page_url('configuration_remove.php', [], $basePath);
    }
}

if (!function_exists('admin_remove_configuration')) {
    function admin_remove_configuration(PDO $pdo, $configId, $folderId) {
        $configId = (int) $configId;
        $folderId = (int) $folderId;
        if ($configId <= 0 || $folderId <= 0) {
            return false;
        }
        $stmt = $pdo->prepare('DELETE FROM door_configurations WHERE id = ? AND folder_id = ?');
        $stmt->execute([$configId, $folderId]);
        return $stmt->rowCount() > 0;
    }
}

if (!function_exists('admin_remove_configurations_from_folder')) {
    function admin_remove_configurations_from_folder(PDO $pdo, $folderId, array $configIds) {
        $folderId = (int) $folderId;
        $configIds = array_values(array_unique(array_filter(array_map('intval', $configIds))));
        if ($folderId <= 0 || empty($configIds)) {
            return 0;
        }
        $placeholders = implode(',', array_fill(0, count($configIds), '?'));
        $stmt = $pdo->prepare("DELETE FROM door_configurations WHERE folder_id = ? AND id IN ($placeholders)");
        $stmt->execute(array_merge([$folderId], $configIds));
        return $stmt->rowCount();
    }
}

if (!function_exists('admin_folder_contact_summary')) {
    function admin_folder_contact_summary(array $folder) {
        $parts = [];
        $name = trim(trim((string)($folder['customer_first_name'] ?? '')) . ' ' . trim((string)($folder['customer_last_name'] ?? '')));
        if ($name !== '') {
            $parts[] = $name;
        }
        $phone = trim((string)($folder['customer_phone'] ?? ''));
        if ($phone !== '') {
            $parts[] = $phone;
        }
        $billing = trim((string)($folder['billing_address'] ?? ''));
        if ($billing !== '') {
            $parts[] = $billing;
        }
        return implode(' · ', $parts);
    }
}

if (!function_exists('admin_hubspot_token_is_valid')) {
    function admin_hubspot_token_is_valid($token) {
        $token = trim((string) $token);
        return $token !== '' && $token !== 'YOUR_PRIVATE_APP_TOKEN_HERE';
    }
}

if (!function_exists('admin_hubspot_config_paths')) {
    function admin_hubspot_config_paths() {
        $root = __DIR__;
        $paths = [];
        if (defined('HUBSPOT_CONFIG_PATH') && HUBSPOT_CONFIG_PATH !== '') {
            $paths[] = HUBSPOT_CONFIG_PATH;
        }
        $paths = array_merge($paths, [
            $root . '/hubspot-config.php',
            $root . '/demo_pandadoc/hubspot/api/hubspot-config.php',
            $root . '/hubspot/api/hubspot-config.php',
            dirname($root) . '/hubspot/api/hubspot-config.php',
            $root . '/../hubspot/api/hubspot-config.php',
            $root . '/hubspot-create-deal.php',
            $root . '/hubspot/api/hubspot-create-deal.php',
            $root . '/demo_pandadoc/hubspot/api/hubspot-create-deal.php',
            dirname($root) . '/hubspot/api/hubspot-create-deal.php',
        ]);
        $unique = [];
        foreach ($paths as $path) {
            $real = realpath($path);
            $key = $real !== false ? $real : $path;
            if (!isset($unique[$key])) {
                $unique[$key] = $path;
            }
        }
        return array_values($unique);
    }
}

if (!function_exists('admin_read_hubspot_token_from_file')) {
    function admin_read_hubspot_token_from_file($path) {
        if (!is_file($path) || !is_readable($path)) {
            return '';
        }
        if (!defined('HUBSPOT_PRIVATE_APP_TOKEN')) {
            require_once $path;
            if (defined('HUBSPOT_PRIVATE_APP_TOKEN') && admin_hubspot_token_is_valid(HUBSPOT_PRIVATE_APP_TOKEN)) {
                return (string) HUBSPOT_PRIVATE_APP_TOKEN;
            }
        }
        $content = @file_get_contents($path);
        if ($content === false) {
            return '';
        }
        if (preg_match('/\$HUBSPOT_PRIVATE_APP_TOKEN\s*=\s*[\'"](pat-[^\'"]+)[\'"]/', $content, $m)) {
            return $m[1];
        }
        if (preg_match('/define\s*\(\s*[\'"]HUBSPOT_PRIVATE_APP_TOKEN[\'"]\s*,\s*[\'"](pat-[^\'"]+)[\'"]\s*\)/', $content, $m)) {
            return $m[1];
        }
        if (preg_match('/[\'"]HUBSPOT_PRIVATE_APP_TOKEN[\'"]\s*,\s*\([^)]*\)\s*\?\s*\$t\s*:\s*[\'"](pat-[^\'"]+)[\'"]/', $content, $m)) {
            return $m[1];
        }
        if (preg_match('/[\'"](pat-[a-z0-9\-]{20,})[\'"]/', $content, $m)) {
            return $m[1];
        }
        return '';
    }
}

if (!function_exists('admin_resolve_hubspot_token')) {
    function admin_resolve_hubspot_token() {
        static $resolved = null;
        if ($resolved !== null) {
            return $resolved;
        }

        $env = getenv('HUBSPOT_PRIVATE_APP_TOKEN');
        if ($env !== false && admin_hubspot_token_is_valid($env)) {
            $resolved = (string) $env;
            return $resolved;
        }

        if (defined('HUBSPOT_PRIVATE_APP_TOKEN') && admin_hubspot_token_is_valid(HUBSPOT_PRIVATE_APP_TOKEN)) {
            $resolved = (string) HUBSPOT_PRIVATE_APP_TOKEN;
            return $resolved;
        }

        foreach (admin_hubspot_config_paths() as $path) {
            $token = admin_read_hubspot_token_from_file($path);
            if (admin_hubspot_token_is_valid($token)) {
                $resolved = $token;
                if (!defined('HUBSPOT_PRIVATE_APP_TOKEN')) {
                    define('HUBSPOT_PRIVATE_APP_TOKEN', $token);
                }
                return $resolved;
            }
        }

        $resolved = '';
        return $resolved;
    }
}

if (!function_exists('admin_load_hubspot_config')) {
    function admin_load_hubspot_config() {
        return admin_hubspot_token_is_valid(admin_resolve_hubspot_token());
    }
}

if (!function_exists('admin_hubspot_json_request')) {
    function admin_hubspot_json_request($method, $path, $body = null) {
        $token = admin_resolve_hubspot_token();
        if (!admin_hubspot_token_is_valid($token)) {
            return ['ok' => false, 'status' => 0, 'body' => null, 'error' => 'HubSpot not configured'];
        }
        $url = 'https://api.hubapi.com' . $path;
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST => strtoupper((string) $method),
            CURLOPT_TIMEOUT => 45,
            CURLOPT_HTTPHEADER => [
                'Authorization: Bearer ' . $token,
                'Content-Type: application/json',
            ],
        ]);
        if ($body !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
        }
        $raw = curl_exec($ch);
        $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlErr = curl_error($ch);
        curl_close($ch);
        if ($raw === false) {
            return ['ok' => false, 'status' => $status, 'body' => null, 'error' => $curlErr ?: 'HubSpot request failed'];
        }
        $decoded = json_decode($raw, true);
        return [
            'ok' => $status >= 200 && $status < 300,
            'status' => $status,
            'body' => is_array($decoded) ? $decoded : null,
            'error' => ($status >= 200 && $status < 300) ? '' : $raw,
        ];
    }
}

if (!function_exists('admin_hubspot_contact_from_search_result')) {
    function admin_hubspot_contact_from_search_result(array $row) {
        $p = $row['properties'] ?? [];
        return [
            'contact_id' => (string) ($row['id'] ?? ''),
            'hubspot_contact_id' => (string) ($row['id'] ?? ''),
            'email' => trim((string) ($p['email'] ?? '')),
            'customer_first_name' => trim((string) ($p['firstname'] ?? '')),
            'customer_last_name' => trim((string) ($p['lastname'] ?? '')),
            'customer_phone' => trim((string) ($p['phone'] ?? '')),
            'billing_address' => trim((string) ($p['address'] ?? '')),
        ];
    }
}

if (!function_exists('admin_find_hubspot_contact_id_by_email')) {
    function admin_find_hubspot_contact_id_by_email($email) {
        $contact = admin_fetch_hubspot_contact_by_email($email);
        return $contact !== null ? ($contact['contact_id'] ?? null) : null;
    }
}

if (!function_exists('admin_fetch_hubspot_contact_by_email')) {
    function admin_fetch_hubspot_contact_by_email($email) {
        $email = trim((string) $email);
        if ($email === '') {
            return null;
        }
        $searchBody = [
            'filterGroups' => [[
                'filters' => [[
                    'propertyName' => 'email',
                    'operator' => 'EQ',
                    'value' => $email,
                ]],
            ]],
            'properties' => ['email', 'firstname', 'lastname', 'phone', 'address'],
            'limit' => 1,
        ];
        $res = admin_hubspot_json_request('POST', '/crm/v3/objects/contacts/search', $searchBody);
        if (!$res['ok'] || !is_array($res['body'])) {
            return null;
        }
        $row = $res['body']['results'][0] ?? null;
        if (!is_array($row) || empty($row['id'])) {
            return null;
        }
        return admin_hubspot_contact_from_search_result($row);
    }
}

if (!function_exists('admin_fetch_hubspot_contact')) {
    function admin_fetch_hubspot_contact($contactId) {
        $contactId = trim((string) $contactId);
        if ($contactId === '') {
            return null;
        }
        $props = 'firstname,lastname,phone,address,email';
        $res = admin_hubspot_json_request('GET', '/crm/v3/objects/contacts/' . rawurlencode($contactId) . '?properties=' . $props, null);
        if (!$res['ok'] || !is_array($res['body'])) {
            return null;
        }
        return admin_hubspot_contact_from_search_result($res['body']);
    }
}

if (!function_exists('admin_upsert_hubspot_contact')) {
    function admin_upsert_hubspot_contact($email, array $fields) {
        $email = trim((string) $email);
        if ($email === '') {
            return ['success' => false, 'contact_id' => null, 'error' => 'Customer email is required to create a HubSpot contact.'];
        }
        if (!admin_load_hubspot_config()) {
            return [
                'success' => false,
                'contact_id' => null,
                'error' => 'HubSpot is not configured. Add HUBSPOT_PRIVATE_APP_TOKEN to config.php, set the env var, or place hubspot-config.php where the admin app can read it.',
            ];
        }

        $props = ['email' => $email];
        $first = trim((string) ($fields['customer_first_name'] ?? $fields['firstname'] ?? ''));
        $last = trim((string) ($fields['customer_last_name'] ?? $fields['lastname'] ?? ''));
        $phone = trim((string) ($fields['customer_phone'] ?? $fields['phone'] ?? ''));
        $address = trim((string) ($fields['billing_address'] ?? $fields['address'] ?? ''));
        if ($first !== '') {
            $props['firstname'] = $first;
        }
        if ($last !== '') {
            $props['lastname'] = $last;
        }
        if ($phone !== '') {
            $props['phone'] = $phone;
        }
        if ($address !== '') {
            $props['address'] = $address;
        }

        $existingId = admin_find_hubspot_contact_id_by_email($email);
        if ($existingId !== null) {
            unset($props['email']);
            $res = admin_hubspot_json_request('PATCH', '/crm/v3/objects/contacts/' . rawurlencode($existingId), ['properties' => $props]);
            if (!$res['ok']) {
                $detail = is_string($res['error'] ?? null) && $res['error'] !== ''
                    ? $res['error']
                    : 'HubSpot contact update failed.';
                return ['success' => false, 'contact_id' => $existingId, 'error' => $detail];
            }
            return ['success' => true, 'contact_id' => $existingId, 'error' => ''];
        }

        $res = admin_hubspot_json_request('POST', '/crm/v3/objects/contacts', ['properties' => $props]);
        if (!$res['ok'] || !is_array($res['body']) || empty($res['body']['id'])) {
            $detail = is_string($res['error'] ?? null) && $res['error'] !== ''
                ? $res['error']
                : 'HubSpot contact create failed.';
            return ['success' => false, 'contact_id' => null, 'error' => $detail];
        }
        return ['success' => true, 'contact_id' => (string) $res['body']['id'], 'error' => ''];
    }
}

if (!function_exists('admin_shopify_is_configured')) {
    function admin_shopify_is_configured() {
        return defined('SHOPIFY_SHOP') && defined('SHOPIFY_ACCESS_TOKEN')
            && SHOPIFY_SHOP !== '' && SHOPIFY_ACCESS_TOKEN !== '';
    }
}

if (!function_exists('admin_shopify_gql_url')) {
    function admin_shopify_gql_url() {
        $apiVersion = defined('SHOPIFY_API_VERSION') && SHOPIFY_API_VERSION !== ''
            ? SHOPIFY_API_VERSION
            : '2024-10';
        return 'https://' . SHOPIFY_SHOP . '/admin/api/' . $apiVersion . '/graphql.json';
    }
}

if (!function_exists('admin_shopify_gql_request')) {
    function admin_shopify_gql_request($query, array $variables = []) {
        if (!admin_shopify_is_configured()) {
            return ['ok' => false, 'error' => 'Shopify API not configured', 'http' => 500, 'data' => null];
        }
        $ch = curl_init(admin_shopify_gql_url());
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => json_encode(['query' => $query, 'variables' => $variables]),
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER     => [
                'Content-Type: application/json',
                'X-Shopify-Access-Token: ' . SHOPIFY_ACCESS_TOKEN,
            ],
            CURLOPT_TIMEOUT        => 30,
        ]);
        $response = curl_exec($ch);
        $err = curl_error($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        if ($err) {
            return ['ok' => false, 'error' => 'Shopify request failed: ' . $err, 'http' => 500, 'data' => null];
        }
        $data = json_decode((string) $response, true);
        if ($code < 200 || $code >= 300) {
            return ['ok' => false, 'error' => 'Shopify HTTP ' . $code, 'http' => $code, 'data' => null];
        }
        if (!empty($data['errors'])) {
            $msg = is_array($data['errors']) ? json_encode($data['errors']) : (string) $data['errors'];
            return ['ok' => false, 'error' => $msg, 'http' => 500, 'data' => null];
        }
        return ['ok' => true, 'error' => '', 'http' => $code, 'data' => $data['data'] ?? []];
    }
}

if (!function_exists('admin_format_shopify_billing_address')) {
    function admin_format_shopify_billing_address(array $addr) {
        $parts = [];
        foreach (['address1', 'address2', 'city', 'province', 'zip', 'country'] as $key) {
            $value = trim((string) ($addr[$key] ?? ''));
            if ($value !== '') {
                $parts[] = $value;
            }
        }
        return implode(', ', $parts);
    }
}

if (!function_exists('admin_shopify_customer_from_node')) {
    function admin_shopify_customer_from_node(array $node) {
        $addr = is_array($node['defaultAddress'] ?? null) ? $node['defaultAddress'] : [];
        $gid = (string) ($node['id'] ?? '');
        $numericId = '';
        if (preg_match('/(\d+)$/', $gid, $m)) {
            $numericId = $m[1];
        }
        return [
            'shopify_customer_id' => $numericId,
            'shopify_customer_gid' => $gid,
            'customer_first_name' => trim((string) ($node['firstName'] ?? '')),
            'customer_last_name' => trim((string) ($node['lastName'] ?? '')),
            'customer_phone' => trim((string) ($node['phone'] ?? '')),
            'billing_address' => admin_format_shopify_billing_address($addr),
        ];
    }
}

if (!function_exists('admin_fetch_shopify_customer_by_email')) {
    function admin_fetch_shopify_customer_by_email($email) {
        $email = trim((string) $email);
        if ($email === '' || !admin_shopify_is_configured()) {
            return null;
        }
        $query = <<<'GQL'
query adminFindCustomerByEmail($query: String!) {
  customers(first: 1, query: $query) {
    edges {
      node {
        id
        email
        firstName
        lastName
        phone
        defaultAddress {
          address1
          address2
          city
          province
          zip
          country
        }
      }
    }
  }
}
GQL;
        $res = admin_shopify_gql_request($query, ['query' => 'email:' . $email]);
        if (!$res['ok']) {
            return null;
        }
        $node = $res['data']['customers']['edges'][0]['node'] ?? null;
        if (!is_array($node) || empty($node['id'])) {
            return null;
        }
        return admin_shopify_customer_from_node($node);
    }
}

if (!function_exists('admin_upsert_shopify_customer')) {
    function admin_upsert_shopify_customer($email, array $fields) {
        $email = trim((string) $email);
        if ($email === '') {
            return ['success' => false, 'customer_id' => null, 'error' => 'Customer email is required.'];
        }
        if (!admin_shopify_is_configured()) {
            return [
                'success' => false,
                'customer_id' => null,
                'error' => 'Shopify API not configured. Set SHOPIFY_SHOP and SHOPIFY_ACCESS_TOKEN in config.php.',
            ];
        }

        $existing = admin_fetch_shopify_customer_by_email($email);
        if ($existing === null || empty($existing['shopify_customer_gid'])) {
            return [
                'success' => false,
                'customer_id' => null,
                'error' => 'No Shopify customer found for this email.',
            ];
        }

        $first = trim((string) ($fields['customer_first_name'] ?? ''));
        $last = trim((string) ($fields['customer_last_name'] ?? ''));
        $phone = trim((string) ($fields['customer_phone'] ?? ''));
        $billing = trim((string) ($fields['billing_address'] ?? ''));

        $addressRow = ['phone' => $phone];
        if ($billing !== '') {
            $addressRow['address1'] = $billing;
        }
        if ($first !== '') {
            $addressRow['firstName'] = $first;
        }
        if ($last !== '') {
            $addressRow['lastName'] = $last;
        }

        $input = [
            'id'        => $existing['shopify_customer_gid'],
            'firstName' => $first,
            'lastName'  => $last,
            'phone'     => $phone,
            'addresses' => [$addressRow],
        ];

        $mutation = <<<'GQL'
mutation adminCustomerContactUpdate($input: CustomerInput!) {
  customerUpdate(input: $input) {
    customer { id email firstName lastName phone }
    userErrors { field message }
  }
}
GQL;
        $res = admin_shopify_gql_request($mutation, ['input' => $input]);
        if (!$res['ok']) {
            return ['success' => false, 'customer_id' => $existing['shopify_customer_id'] ?? null, 'error' => $res['error']];
        }
        $userErrors = $res['data']['customerUpdate']['userErrors'] ?? [];
        if (!empty($userErrors)) {
            return [
                'success' => false,
                'customer_id' => $existing['shopify_customer_id'] ?? null,
                'error' => implode('; ', array_column($userErrors, 'message')),
            ];
        }
        return [
            'success' => true,
            'customer_id' => $existing['shopify_customer_id'] ?? null,
            'error' => '',
        ];
    }
}

if (!function_exists('admin_load_folder_contact_for_display')) {
    /**
     * Load contact fields from Shopify customer (nothing persisted locally).
     */
    function admin_load_folder_contact_for_display(array $folder) {
        $email = trim((string) ($folder['customer_email'] ?? ''));
        if ($email === '') {
            return $folder;
        }
        $remote = admin_fetch_shopify_customer_by_email($email);
        if ($remote === null) {
            return $folder;
        }
        $merged = array_merge($folder, $remote);
        $hubspot = admin_fetch_hubspot_contact_by_email($email);
        if (is_array($hubspot) && !empty($hubspot['hubspot_contact_id'])) {
            $merged['hubspot_contact_id'] = $hubspot['hubspot_contact_id'];
        }
        return $merged;
    }
}

if (!function_exists('admin_resolve_hubspot_deal_endpoint')) {
    function admin_resolve_hubspot_deal_endpoint() {
        if (defined('HUBSPOT_DEAL_ENDPOINT') && HUBSPOT_DEAL_ENDPOINT !== '') {
            return (string) HUBSPOT_DEAL_ENDPOINT;
        }
        $scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
        $host = $_SERVER['HTTP_HOST'] ?? '';
        if ($host === '') {
            return '';
        }
        return $scheme . '://' . $host . '/hubspot/api/hubspot-create-deal.php';
    }
}

if (!function_exists('admin_post_json_to_url')) {
    function admin_post_json_to_url($url, array $payload, $timeout = 15) {
        $url = trim((string) $url);
        if ($url === '' || !function_exists('curl_init')) {
            return ['ok' => false, 'body' => null, 'error' => 'HTTP client not available'];
        }
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_POST           => true,
            CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
            CURLOPT_POSTFIELDS     => json_encode($payload),
            CURLOPT_CONNECTTIMEOUT => 5,
            CURLOPT_TIMEOUT        => max(5, (int) $timeout),
        ]);
        $raw = curl_exec($ch);
        $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $err = curl_error($ch);
        curl_close($ch);
        if ($raw === false) {
            return ['ok' => false, 'body' => null, 'error' => $err ?: 'Request failed'];
        }
        $decoded = json_decode($raw, true);
        $ok = $code >= 200 && $code < 300 && is_array($decoded) && !empty($decoded['success']);
        return [
            'ok' => $ok,
            'body' => is_array($decoded) ? $decoded : null,
            'error' => $ok ? '' : (is_array($decoded) && !empty($decoded['error']) ? (string) $decoded['error'] : $raw),
        ];
    }
}

if (!function_exists('admin_sync_configuration_to_hubspot_deal')) {
    /**
     * Push one saved configuration to the HubSpot deal (same payload as configuration_detail save).
     */
    function admin_sync_configuration_to_hubspot_deal(array $config, array $folder) {
        $savedProjectName = trim((string) ($folder['name'] ?? ''));
        if ($savedProjectName === '') {
            return ['success' => false, 'error' => 'Project name is required for HubSpot sync.'];
        }
        $endpoint = admin_resolve_hubspot_deal_endpoint();
        if ($endpoint === '') {
            return ['success' => false, 'error' => 'HubSpot deal endpoint is not configured.'];
        }

        $options = json_decode((string) ($config['options_json'] ?? ''), true);
        if (!is_array($options)) {
            $options = [];
        }
        $configId = (int) ($config['id'] ?? 0);
        $computedPrice = (float) ($config['computed_price'] ?? 0);
        $basePrice = (float) ($config['base_price'] ?? 0);
        $hsLineProps = admin_get_merged_line_item_properties($options, $configId);
        $hsOrderTotal = admin_get_order_total_price($options, $computedPrice);
        $email = trim((string) ($folder['customer_email'] ?? ($config['customer_email'] ?? '')));

        $payload = [
            'product_id'           => (string) ($options['_product_id'] ?? ($config['product_id'] ?? '')),
            'product_handle'       => (string) ($options['_product_handle'] ?? ($config['product_handle'] ?? '')),
            'product_title'        => (string) ($options['_product_title'] ?? ($config['product_title'] ?? 'Door Configuration')),
            'product_sku'          => (string) ($options['_product_sku'] ?? ($config['product_sku'] ?? '')),
            'product_type'         => (string) ($options['_product_type'] ?? ($config['product_type'] ?? '')),
            'options'              => $options,
            'options_summary'      => admin_build_options_values_only_text($options),
            'line_item_properties' => $hsLineProps,
            'quantity'             => max(1, (int) ($options['_quantity'] ?? 1)),
            'base_price'           => $basePrice,
            'applied_price'        => (float) $hsOrderTotal,
            'price'                => (float) $hsOrderTotal,
            'currency'             => 'USD',
            'customer_email'       => $email,
            'customer_first_name'  => (string) ($folder['customer_first_name'] ?? ''),
            'customer_last_name'   => (string) ($folder['customer_last_name'] ?? ''),
            'call_requested'       => '--',
            'saved_project_name'   => $savedProjectName,
            'config_id'            => (string) $configId,
        ];

        $res = admin_post_json_to_url($endpoint, $payload);
        if (!$res['ok']) {
            return ['success' => false, 'error' => $res['error'] ?: 'HubSpot deal sync failed.'];
        }
        return ['success' => true, 'error' => '', 'deal_id' => (string) ($res['body']['dealId'] ?? $res['body']['deal_id'] ?? '')];
    }
}

if (!function_exists('admin_normalize_quote_image_url')) {
    /** Ensure image URLs work in PDF/email (dompdf needs absolute https). */
    function admin_normalize_quote_image_url($url) {
        $url = trim((string) $url);
        if ($url === '') {
            return '';
        }
        if (strpos($url, '//') === 0) {
            return 'https:' . $url;
        }
        return $url;
    }
}

if (!function_exists('admin_config_quote_image_url')) {
    /** Prefer admin custom image, then stored product default image. */
    function admin_config_quote_image_url(array $options, array $config = []) {
        foreach (['_custom_image_url', 'custom_image_url'] as $key) {
            $custom = admin_normalize_quote_image_url($options[$key] ?? '');
            if ($custom !== '') {
                return $custom;
            }
        }
        foreach (['_product_image_url', 'product_image_url', 'productImageUrl'] as $key) {
            $product = admin_normalize_quote_image_url($options[$key] ?? '');
            if ($product !== '') {
                return $product;
            }
        }
        foreach (['product_image_url', 'product_image', 'image_url'] as $key) {
            $product = admin_normalize_quote_image_url($config[$key] ?? '');
            if ($product !== '') {
                return $product;
            }
        }
        return '';
    }
}

if (!function_exists('admin_configuration_options_from_form_post')) {
    /**
     * Build options and prices from configuration detail form POST (does not persist).
     *
     * @return array{options: array, base_price: float, computed_price: float}
     */
    function admin_configuration_options_from_form_post(array $options, array $config, array $post) {
        $schema = $options['_pricing_schema'] ?? [];
        if (!empty($post['opt_hardware_options_json'])) {
            $hwDecoded = json_decode((string) $post['opt_hardware_options_json'], true);
            if (is_array($hwDecoded)) {
                $options['_hardware_selections'] = $hwDecoded;
                $options['hardware_options'] = $hwDecoded;
            }
        }
        if (array_key_exists('opt_casing_options_json', $post)) {
            $casingDecoded = json_decode((string) $post['opt_casing_options_json'], true);
            $existingCasing = admin_resolve_casing_selections($options);
            $casingNormalized = admin_merge_casing_admin_post(
                $existingCasing,
                is_array($casingDecoded) ? $casingDecoded : []
            );
            if (!empty($casingNormalized)) {
                $options['_casing_selections'] = $casingNormalized;
                $options['casing_options'] = $casingNormalized;
                if (count($casingNormalized) === 1) {
                    $options['_casing_selection'] = $casingNormalized[0];
                } else {
                    unset($options['_casing_selection']);
                }
            } else {
                unset($options['_casing_selections'], $options['casing_options'], $options['_casing_selection']);
            }
        }
        foreach ($schema as $opt) {
            if (!is_array($opt) || empty($opt['id'])) {
                continue;
            }
            $oid = $opt['id'];
            $optType = strtolower((string) ($opt['type'] ?? $opt['input_type'] ?? ''));
            if ($optType === 'hardware_multi' || $optType === 'casing_multi') {
                continue;
            }
            if ((string) $oid === 'sidelight_design') {
                $options[$oid] = !empty($post['opt_sidelight_design']) ? ['1'] : [];
                continue;
            }
            if ($optType === 'checkbox') {
                $options[$oid] = isset($post['opt_' . $oid]) && is_array($post['opt_' . $oid])
                    ? array_values(array_filter($post['opt_' . $oid]))
                    : [];
            } elseif ($optType === 'number') {
                $v = $post['opt_' . $oid] ?? '';
                $options[$oid] = $v !== '' ? (float) $v : null;
            } else {
                $v = $post['opt_' . $oid] ?? '';
                $options[$oid] = $v !== '' ? $v : null;
            }
        }

        $basePrice = isset($post['base_price']) ? (float) $post['base_price'] : (float) ($config['base_price'] ?? 0);
        $orderTotalOverrideRaw = trim((string) ($post['admin_order_total_override'] ?? ''));
        $lineItemPropsOverride = trim((string) ($post['admin_line_item_properties_override'] ?? ''));

        if ($orderTotalOverrideRaw !== '' && is_numeric($orderTotalOverrideRaw)) {
            $options['_admin_order_total_override'] = round((float) $orderTotalOverrideRaw, 2);
        } else {
            unset($options['_admin_order_total_override']);
        }
        if ($lineItemPropsOverride !== '') {
            $options['_admin_line_item_properties_override'] = $lineItemPropsOverride;
        } else {
            unset($options['_admin_line_item_properties_override']);
        }

        $customImageUrl = trim((string) ($post['custom_image_url'] ?? ''));
        if ($customImageUrl !== '') {
            $options['_custom_image_url'] = $customImageUrl;
        } else {
            unset($options['_custom_image_url']);
        }
        $internalImagesJson = trim((string) ($post['internal_images_json'] ?? ''));
        if ($internalImagesJson !== '') {
            $internalDecoded = json_decode($internalImagesJson, true);
            $internalUrls = admin_normalize_internal_image_urls(is_array($internalDecoded) ? $internalDecoded : []);
            if (!empty($internalUrls)) {
                $options['_internal_images'] = $internalUrls;
            } else {
                unset($options['_internal_images']);
            }
        } else {
            unset($options['_internal_images']);
        }

        $options = admin_save_draft_order_title_from_post($options, $post);
        $options = admin_merge_shopify_casing_into_options($options);
        $options = admin_sync_listing_selected_summary($options);

        $qty = max(1, (int) ($options['_quantity'] ?? 1));
        if (isset($options['_admin_order_total_override'])) {
            $computedPrice = round((float) $options['_admin_order_total_override'] / $qty, 2);
        } else {
            $computedPrice = admin_get_applied_unit_price($options, $basePrice, isset($config['computed_price']) ? (float) $config['computed_price'] : null);
        }

        return [
            'options' => $options,
            'base_price' => $basePrice,
            'computed_price' => $computedPrice,
        ];
    }
}

if (!function_exists('admin_render_configuration_quote_preview')) {
    /**
     * Render single-configuration preview with editable title + description.
     */
    function admin_render_configuration_quote_preview(PDO $pdo, array $config, $folderId = 0) {
        $configId = (int) ($config['id'] ?? 0);
        if ($configId <= 0) {
            header('Content-Type: text/html; charset=utf-8');
            echo '<!DOCTYPE html><html><body><p>Invalid configuration.</p></body></html>';
            exit;
        }

        $opts = json_decode((string) ($config['options_json'] ?? ''), true);
        if (!is_array($opts)) {
            $opts = [];
        }

        $productTitle = admin_get_configuration_quote_title($opts, $config);
        $description = admin_get_quote_description_text($opts, $config);

        $effectiveFolderId = (int) $folderId;
        if ($effectiveFolderId <= 0 && !empty($config['folder_id'])) {
            $effectiveFolderId = (int) $config['folder_id'];
        }

        $canSave = ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST' && !empty($_POST);
        $saveAction = 'configuration_quote_preview.php';

        $hiddenInputs = '<input type="hidden" name="id" value="' . (int) $configId . '">';
        if ($effectiveFolderId > 0) {
            $hiddenInputs .= '<input type="hidden" name="folder_id" value="' . (int) $effectiveFolderId . '">';
        }
        if ($canSave) {
            foreach ($_POST as $k => $v) {
                if (in_array((string) $k, ['action', 'staff_quote_title', 'staff_quote_description'], true)) {
                    continue;
                }
                if (is_array($v)) {
                    foreach ($v as $vv) {
                        if (is_array($vv)) {
                            continue;
                        }
                        $hiddenInputs .= '<input type="hidden" name="' . htmlspecialchars((string) $k . '[]', ENT_QUOTES, 'UTF-8')
                            . '" value="' . htmlspecialchars((string) $vv, ENT_QUOTES, 'UTF-8') . '">';
                    }
                } else {
                    $hiddenInputs .= '<input type="hidden" name="' . htmlspecialchars((string) $k, ENT_QUOTES, 'UTF-8')
                        . '" value="' . htmlspecialchars((string) $v, ENT_QUOTES, 'UTF-8') . '">';
                }
            }
        }

        header('Content-Type: text/html; charset=utf-8');
        echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
            . '<meta name="viewport" content="width=device-width, initial-scale=1">'
            . '<title>Quote preview — Config #' . (int) $configId . '</title>'
            . '<style>'
            . 'body{font-family:Arial,sans-serif;background:#f8fafc;margin:0;padding:1.5rem;color:#0f172a;}'
            . '.wrap{max-width:720px;margin:0 auto;}'
            . '.field-label{display:block;font-size:0.8rem;font-weight:700;color:#475569;margin:0 0 0.35rem 0;}'
            . '.title-input{width:100%;padding:0.7rem 0.9rem;border:1px solid #e2e8f0;border-radius:10px;'
            . 'background:#fff;font:inherit;font-size:1rem;font-weight:700;box-sizing:border-box;margin-bottom:1rem;}'
            . 'textarea{width:100%;min-height:220px;padding:1rem 1.1rem;border:1px solid #e2e8f0;border-radius:10px;'
            . 'background:#fff;font:inherit;font-size:0.95rem;line-height:1.55;resize:vertical;box-sizing:border-box;}'
            . '.save-row{margin-top:1rem;}'
            . '.save-btn{display:inline-block;padding:10px 16px;background:#0f766e;color:#fff;border:0;border-radius:10px;'
            . 'font-weight:700;cursor:pointer;}'
            . '.hint{margin-top:0.75rem;font-size:0.85rem;color:#64748b;}'
            . '</style></head><body><div class="wrap">'
            . ($canSave
                ? '<form method="post" action="' . htmlspecialchars($saveAction, ENT_QUOTES, 'UTF-8') . '">'
                    . '<input type="hidden" name="action" value="save_quote_preview">'
                    . $hiddenInputs
                    . '<label class="field-label" for="staff-quote-title">Product title</label>'
                    . '<input id="staff-quote-title" class="title-input" type="text" name="staff_quote_title" value="'
                    . htmlspecialchars($productTitle, ENT_QUOTES, 'UTF-8') . '">'
                    . '<label class="field-label" for="staff-quote-description">Description</label>'
                    . '<textarea id="staff-quote-description" name="staff_quote_description">'
                    . htmlspecialchars($description, ENT_QUOTES, 'UTF-8')
                    . '</textarea>'
                    . '<div class="save-row"><button type="submit" class="save-btn">Save</button></div>'
                    . '<p class="hint">Edits here are display-only for quote preview/PDF/email. '
                    . 'They do not change values on the configuration page.</p>'
                    . '</form>'
                : '<label class="field-label">Product title</label>'
                    . '<input class="title-input" type="text" readonly value="' . htmlspecialchars($productTitle, ENT_QUOTES, 'UTF-8') . '">'
                    . '<label class="field-label">Description</label>'
                    . '<textarea readonly>' . htmlspecialchars($description, ENT_QUOTES, 'UTF-8') . '</textarea>'
                    . '<div class="save-row" style="font-size:0.9rem;color:#64748b;">'
                    . '(Open preview from the configuration page to edit and save.)</div>')
            . '</div></body></html>';
        exit;
    }
}

// =============================================================================
// PDF HTML — quote-pdf.html in admin/includes/ (admin_config_helpers.php is in admin/)
// Placeholders {{name}} are filled at runtime. Do not remove @section markers.
// =============================================================================

if (!function_exists('admin_quote_fill_template')) {
    function admin_quote_fill_template($template, array $vars) {
        $search = [];
        $replace = [];
        foreach ($vars as $key => $value) {
            $search[] = '{{' . $key . '}}';
            $replace[] = (string) $value;
        }
        return str_replace($search, $replace, (string) $template);
    }
}

if (!function_exists('admin_quote_h')) {
    function admin_quote_h($value) {
        return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
    }
}

if (!function_exists('admin_quote_pdf_template_file')) {
    /** quote-pdf.html in admin/includes/ when admin_config_helpers.php lives in admin/. */
    function admin_quote_pdf_template_file() {
        if (defined('QUOTE_PDF_TEMPLATE_FILE') && trim((string) QUOTE_PDF_TEMPLATE_FILE) !== '') {
            return str_replace('\\', '/', (string) QUOTE_PDF_TEMPLATE_FILE);
        }
        return str_replace('\\', '/', __DIR__ . '/includes/quote-pdf.html');
    }
}

if (!function_exists('admin_quote_pdf_template_sections')) {
    /** @return array<string, string> */
    function admin_quote_pdf_template_sections() {
        static $sections = null;
        if (is_array($sections)) {
            return $sections;
        }
        $sections = [];
        $path = admin_quote_pdf_template_file();
        if (!is_file($path)) {
            return $sections;
        }
        $content = (string) file_get_contents($path);
        if (preg_match_all(
            '/<!--\s*@section:([a-z0-9_-]+)\s*-->(.*?)<!--\s*@endsection\s*-->/is',
            $content,
            $matches,
            PREG_SET_ORDER
        )) {
            foreach ($matches as $match) {
                $sections[(string) $match[1]] = trim((string) $match[2]);
            }
        }
        return $sections;
    }
}

if (!function_exists('admin_quote_load_pdf_template')) {
    function admin_quote_load_pdf_template($name) {
        static $map = [
            'quote-document'      => 'document',
            'quote-order-info'    => 'order-info',
            'quote-address-block' => 'address-block',
            'quote-bill-ship'     => 'bill-ship',
            'quote-items-table'   => 'items-table',
            'quote-items-row'     => 'items-row',
            'quote-totals'        => 'totals',
            'document'            => 'document',
            'order-info'          => 'order-info',
            'address-block'       => 'address-block',
            'bill-ship'           => 'bill-ship',
            'items-table'         => 'items-table',
            'items-row'           => 'items-row',
            'items-row-image'     => 'items-row-image',
            'items-row-image-placeholder' => 'items-row-image-placeholder',
            'totals'              => 'totals',
        ];
        $key = preg_replace('/[^a-z0-9_-]/i', '', (string) $name);
        $section = $map[$key] ?? $key;
        $sections = admin_quote_pdf_template_sections();
        return (string) ($sections[$section] ?? '');
    }
}

if (!function_exists('admin_quote_template_pdf_document')) {
    function admin_quote_template_pdf_document() {
        return admin_quote_load_pdf_template('quote-document');
    }
}

if (!function_exists('admin_quote_template_pdf_order_info')) {
    function admin_quote_template_pdf_order_info() {
        return admin_quote_load_pdf_template('quote-order-info');
    }
}

if (!function_exists('admin_quote_template_pdf_address_block')) {
    function admin_quote_template_pdf_address_block() {
        return admin_quote_load_pdf_template('quote-address-block');
    }
}

if (!function_exists('admin_quote_template_pdf_bill_ship')) {
    function admin_quote_template_pdf_bill_ship() {
        return admin_quote_load_pdf_template('quote-bill-ship');
    }
}

if (!function_exists('admin_quote_template_pdf_items_table')) {
    function admin_quote_template_pdf_items_table() {
        return admin_quote_load_pdf_template('quote-items-table');
    }
}

if (!function_exists('admin_quote_template_pdf_items_row')) {
    function admin_quote_template_pdf_items_row() {
        return admin_quote_load_pdf_template('quote-items-row');
    }
}

if (!function_exists('admin_quote_template_pdf_totals')) {
    function admin_quote_template_pdf_totals() {
        return admin_quote_load_pdf_template('quote-totals');
    }
}

if (!function_exists('admin_quote_template_pdf_table')) {
    /** Line-items table wrapper (injected into PDF document). */
    function admin_quote_template_pdf_table()
    {
        return <<<'HTML'
<div style="font-family:DejaVu Sans,Arial,sans-serif;color:#0f172a;">
{{heading}}
<table cellpadding="0" cellspacing="0" style="width:100%;border-collapse:collapse;border:1px solid #e2e8f0;">
<thead>
<tr>
    <th style="{{th_style}} width:36px; max-width:36px;">#</th>
    <th style="{{th_center}} ">Image</th>
    <th style="{{th_style}}">Product &amp; options</th>
    <th style="{{th_center}} width:44px; max-width:44px;">Qty 222</th>
    <th style="{{th_right}} width:84px; max-width:84px;">Line total</th>
</tr>
</thead>
<tbody>
{{rows_html}}
</tbody>
<tfoot>
<tr>
    <td colspan="4" style="{{foot_label}}">Total</td>
    <td style="{{foot_value}} text-align:right;">{{total_formatted}}</td>
</tr>
</tfoot>
</table>
</div>
HTML;
    }
}

if (!function_exists('admin_quote_template_pdf_table_row')) {
    /** Single product row inside the line-items table. */
    function admin_quote_template_pdf_table_row()
    {
        return <<<'HTML'
<tr>
    <td style="{{cell_style}} width:36px;">#{{config_id}}</td>
    <td style="{{cell_style}} text-align:center; width:84px;">{{image_html}}</td>
    <td style="{{cell_style}}">{{product_html}}</td>
    <td style="{{cell_style}} text-align:center; width:44px;">{{qty}}</td>
    <td style="{{cell_style}} text-align:right; width:84px; font-weight:bold; color:{{brand_color}};">${{line_total}}</td>
</tr>
HTML;
    }
}

if (!function_exists('admin_quote_template_pdf_customer_box')) {
    /** Customer details wrapper. */
    function admin_quote_template_pdf_customer_box()
    {
        return <<<'HTML'
<div style="margin-bottom:18px;">
    <div style="padding:12px 14px;background-color:#f8fafc;border:1px solid #e2e8f0;border-left:4px solid {{brand_color}};">
        {{fields_html}}
    </div>
</div>
HTML;
    }
}

if (!function_exists('admin_quote_template_pdf_customer_field')) {
    /** One customer field (name, email, phone, address). */
    function admin_quote_template_pdf_customer_field()
    {
        return <<<'HTML'
<div style="margin-bottom:{{margin_bottom}};">
    <div style="{{label_style}}">{{label}}</div>
    <div style="{{value_style}}">{{value}}</div>
</div>
HTML;
    }
}

if (!function_exists('admin_quote_template_pdf_product')) {
    function admin_quote_template_pdf_product()
    {
        return <<<'HTML'
<div style="font-size:10.5pt;font-weight:bold;color:#0f172a;line-height:1.35;margin:0 0 4px 0;">{{title}}</div>
{{options_html}}
HTML;
    }
}

if (!function_exists('admin_quote_template_pdf_description')) {
    function admin_quote_template_pdf_description()
    {
        return <<<'HTML'
<div class="quote-description" style="margin-top:{{margin_top}};font-size:{{font_size}};line-height:1.45;color:#475569;word-wrap:break-word;">{{text}}</div>
HTML;
    }
}

if (!function_exists('admin_quote_template_pdf_image')) {
    function admin_quote_template_pdf_image()
    {
        return <<<'HTML'
<img src="{{image_url}}" alt="{{title}}" width="{{width}}" height="{{height}}"
     style="display:block;width:{{width}}px;height:{{height}}px;max-width:{{width}}px;max-height:{{height}}px;border:1px solid #cbd5e1;border-radius:4px;background:#ffffff;">
HTML;
    }
}

if (!function_exists('admin_quote_template_pdf_image_placeholder')) {
    function admin_quote_template_pdf_image_placeholder()
    {
        return <<<'HTML'
<div style="width:72px;height:84px;background-color:#f1f5f9;border:1px dashed #cbd5e1;border-radius:4px;text-align:center;line-height:84px;color:#94a3b8;font-size:8pt;">—</div>
HTML;
    }
}

// =============================================================================
// End of PDF HTML TEMPLATES
// =============================================================================

if (!function_exists('admin_quote_pdf_brand_name')) {
    function admin_quote_pdf_brand_name() {
        if (defined('QUOTE_PDF_BRAND_NAME') && trim((string) QUOTE_PDF_BRAND_NAME) !== '') {
            return trim((string) QUOTE_PDF_BRAND_NAME);
        }
        return 'Vintage Doors';
    }
}

if (!function_exists('admin_quote_pdf_generated_footer')) {
    function admin_quote_pdf_generated_footer() {
        return admin_quote_pdf_brand_name() . ' · Generated ' . date('F j, Y');
    }
}

if (!function_exists('admin_quote_pdf_brand_tagline')) {
    function admin_quote_pdf_brand_tagline() {
        if (defined('QUOTE_PDF_BRAND_TAGLINE') && trim((string) QUOTE_PDF_BRAND_TAGLINE) !== '') {
            return trim((string) QUOTE_PDF_BRAND_TAGLINE);
        }
        return 'Custom door quote';
    }
}

if (!function_exists('admin_quote_pdf_brand_color')) {
    function admin_quote_pdf_brand_color() {
        if (defined('QUOTE_PDF_BRAND_COLOR') && trim((string) QUOTE_PDF_BRAND_COLOR) !== '') {
            return trim((string) QUOTE_PDF_BRAND_COLOR);
        }
        return '#002031';
    }
}

if (!function_exists('admin_quote_pdf_logo_url')) {
    /** White logo for PDF header (absolute https URL; Dompdf needs remote images enabled). */
    function admin_quote_pdf_logo_url() {
        if (defined('QUOTE_PDF_LOGO_URL') && trim((string) QUOTE_PDF_LOGO_URL) !== '') {
            return admin_normalize_quote_image_url(QUOTE_PDF_LOGO_URL);
        }
        return 'https://cdn.shopify.com/s/files/1/0784/9059/9679/files/Vintage_Doors_Logo_-_Main_White.svg?v=1762238320';
    }
}

if (!function_exists('admin_quote_pdf_navy_color')) {
    function admin_quote_pdf_navy_color() {
        if (defined('QUOTE_PDF_NAVY_COLOR') && trim((string) QUOTE_PDF_NAVY_COLOR) !== '') {
            return trim((string) QUOTE_PDF_NAVY_COLOR);
        }
        return '#002031';
    }
}

if (!function_exists('admin_quote_pdf_format_money')) {
    function admin_quote_pdf_format_money($amount) {
        return '$' . number_format((float) $amount, 2);
    }
}

if (!function_exists('admin_quote_pdf_order_number')) {
    function admin_quote_pdf_order_number(array $folder, array $configs = []) {
        $folderId = (int) ($folder['id'] ?? 0);
        if ($folderId > 0) {
            return (string) $folderId;
        }
        if (!empty($configs[0]['id'])) {
            return (string) (int) $configs[0]['id'];
        }
        return '—';
    }
}

if (!function_exists('admin_quote_pdf_sales_person')) {
    function admin_quote_pdf_sales_person(array $configs = []) {
        foreach ($configs as $config) {
            $notes = trim((string) ($config['notes_sales'] ?? ''));
            if ($notes !== '') {
                return $notes;
            }
        }
        if (defined('QUOTE_PDF_SALES_PERSON') && trim((string) QUOTE_PDF_SALES_PERSON) !== '') {
            return trim((string) QUOTE_PDF_SALES_PERSON);
        }
        return 'Asher';
    }
}

if (!function_exists('admin_quote_pdf_sales_extension')) {
    function admin_quote_pdf_sales_extension() {
        if (defined('QUOTE_PDF_SALES_EXTENSION') && trim((string) QUOTE_PDF_SALES_EXTENSION) !== '') {
            return trim((string) QUOTE_PDF_SALES_EXTENSION);
        }
        return '224';
    }
}

if (!function_exists('admin_quote_pdf_description_lines_html')) {
    function admin_quote_pdf_description_lines_html(array $options) {
        $segments = admin_build_quote_description_segments($options);
        $segments = admin_apply_quote_description_overrides($segments, admin_get_quote_description_overrides($options));
        $lines = [];
        foreach ($segments as $segment) {
            if (!is_array($segment) || !empty($segment['__measurement_block__'])) {
                continue;
            }
            $value = trim((string) ($segment['display_value'] ?? ''));
            if ($value !== '') {
                $lines[] = admin_quote_h($value);
            }
        }
        return implode(' / ', $lines);
    }
}

if (!function_exists('admin_quote_pdf_address_body_html')) {
    function admin_quote_pdf_address_body_html(array $customer) {
        $parts = [];
        $name = trim((string) ($customer['name'] ?? ''));
        if ($name !== '') {
            $parts[] = '<strong>' . admin_quote_h($name) . '</strong>';
        }
        $address = trim((string) ($customer['address'] ?? ''));
        if ($address !== '') {
            $parts[] = nl2br(admin_quote_h($address));
        }
        $phone = trim((string) ($customer['phone'] ?? ''));
        if ($phone !== '') {
            $parts[] = 'Phone: ' . admin_quote_h($phone);
        }
        $email = trim((string) ($customer['email'] ?? ''));
        if ($email !== '') {
            $parts[] = 'Email: ' . admin_quote_h($email);
        }
        return !empty($parts) ? implode('<br>', $parts) : '—';
    }
}

if (!function_exists('admin_build_quote_pdf_order_section_html')) {
    function admin_build_quote_pdf_order_section_html(array $folder, array $configs = [], array $quote = []) {
        $subtotal = 0.0;
        foreach ($configs as $config) {
            $opts = json_decode((string) ($config['options_json'] ?? ''), true);
            if (!is_array($opts)) {
                $opts = [];
            }
            $unitPrice = (float) ($config['computed_price'] ?? 0);
            $subtotal += admin_get_order_total_price($opts, $unitPrice);
        }

        $project = trim((string) ($quote['project_name'] ?? $folder['name'] ?? ''));
        if ($project === '') {
            $project = '—';
        }

        return admin_quote_fill_template(admin_quote_template_pdf_order_info(), [
            'project'         => admin_quote_h($project),
            'date'            => admin_quote_h(gmdate('m/d/Y')),
            'items'           => (int) count($configs),
            'estimated_total' => admin_quote_h(admin_quote_pdf_format_money($subtotal)),
        ]);
    }
}

if (!function_exists('admin_build_quote_pdf_bill_ship_html')) {
    function admin_build_quote_pdf_bill_ship_html(array $customer) {
        $body = admin_quote_pdf_address_body_html($customer);
        $block = admin_quote_fill_template(admin_quote_template_pdf_address_block(), [
            'title'     => 'BILL TO',
            'body_html' => $body,
        ]);
        $shipBlock = admin_quote_fill_template(admin_quote_template_pdf_address_block(), [
            'title'     => 'SHIP TO',
            'body_html' => $body,
        ]);
        return admin_quote_fill_template(admin_quote_template_pdf_bill_ship(), [
            'bill_html' => $block,
            'ship_html' => $shipBlock,
        ]);
    }
}

if (!function_exists('admin_build_quote_pdf_items_and_totals_html')) {
    function admin_build_quote_pdf_items_and_totals_html(array $configs) {
        $rowsHtml = '';
        $subtotal = 0.0;
        $rowNum = 0;

        foreach ($configs as $config) {
            $opts = json_decode((string) ($config['options_json'] ?? ''), true);
            if (!is_array($opts)) {
                $opts = [];
            }
            $rowNum++;
            $title = admin_get_configuration_quote_title($opts, $config);
            $unitPrice = (float) ($config['computed_price'] ?? 0);
            $lineTotal = admin_get_order_total_price($opts, $unitPrice);
            $qty = max(1, (int) ($opts['_quantity'] ?? 1));
            $subtotal += $lineTotal;
            $descriptionLines = admin_quote_pdf_description_lines_html($opts);
            $descriptionHtml = '<strong>' . admin_quote_h($title) . '</strong>';
            if ($descriptionLines !== '') {
                $descriptionHtml .= '<br>' . $descriptionLines;
            }

            $configId = (int) ($config['id'] ?? 0);
            $productId = $configId > 0 ? '#' . $configId : '—';
            $imageUrl = admin_config_quote_image_url($opts, $config);
            $imageCellHtml = admin_build_quote_line_image_cell_html($imageUrl, $title);

            $rowsHtml .= admin_quote_fill_template(admin_quote_template_pdf_items_row(), [
                'row_class'        => ($rowNum % 2 === 0) ? 'pdf-tr-even' : 'pdf-tr-odd',
                'product_id'       => admin_quote_h($productId),
                'image_html'       => $imageCellHtml,
                'description_html' => $descriptionHtml,
                'price_each'       => admin_quote_h(admin_quote_pdf_format_money($unitPrice)),
                'qty'              => (int) $qty,
                'line_total'       => admin_quote_h(admin_quote_pdf_format_money($lineTotal)),
            ]);
        }

        $taxDisplay = '—';
        $total = $subtotal;
        $totalsHtml = admin_quote_fill_template(admin_quote_template_pdf_totals(), [
            'subtotal' => admin_quote_h(admin_quote_pdf_format_money($subtotal)),
            'shipping' => 'FREE',
            'tax'      => admin_quote_h($taxDisplay),
            'total'    => admin_quote_h(admin_quote_pdf_format_money($total)),
        ]);

        $tableHtml = admin_quote_fill_template(admin_quote_template_pdf_items_table(), [
            'rows_html' => $rowsHtml,
        ]);

        return $tableHtml . $totalsHtml;
    }
}

if (!function_exists('admin_collect_quote_customer_details')) {
    /**
     * Customer fields for quote PDF/email (folder + Shopify-enriched folder + HubSpot fallback).
     *
     * @return array{name: string, email: string, phone: string, address: string}
     */
    function admin_collect_quote_customer_details(array $folder, $email = '') {
        $email = trim($email !== '' ? (string) $email : (string) ($folder['customer_email'] ?? ''));
        $first = trim((string) ($folder['customer_first_name'] ?? ''));
        $last = trim((string) ($folder['customer_last_name'] ?? ''));
        $phone = trim((string) ($folder['customer_phone'] ?? ''));
        $address = trim((string) ($folder['billing_address'] ?? ''));

        if ($email !== '' && ($first === '' || $last === '' || $phone === '' || $address === '')) {
            $hs = admin_fetch_hubspot_contact_by_email($email);
            if (is_array($hs)) {
                if ($first === '') {
                    $first = trim((string) ($hs['customer_first_name'] ?? ''));
                }
                if ($last === '') {
                    $last = trim((string) ($hs['customer_last_name'] ?? ''));
                }
                if ($phone === '') {
                    $phone = trim((string) ($hs['customer_phone'] ?? ''));
                }
                if ($address === '') {
                    $address = trim((string) ($hs['billing_address'] ?? ''));
                }
            }
        }

        if ($email !== '' && ($first === '' || $phone === '' || $address === '')) {
            $shopify = admin_fetch_shopify_customer_by_email($email);
            if (is_array($shopify)) {
                if ($first === '') {
                    $first = trim((string) ($shopify['customer_first_name'] ?? ''));
                }
                if ($last === '') {
                    $last = trim((string) ($shopify['customer_last_name'] ?? ''));
                }
                if ($phone === '') {
                    $phone = trim((string) ($shopify['customer_phone'] ?? ''));
                }
                if ($address === '') {
                    $address = trim((string) ($shopify['billing_address'] ?? ''));
                }
            }
        }

        $name = trim($first . ' ' . $last);
        return [
            'name'    => $name,
            'email'   => $email,
            'phone'   => $phone,
            'address' => $address,
        ];
    }
}

if (!function_exists('admin_build_quote_customer_info_html')) {
    function admin_build_quote_customer_info_html(array $customer, $forPdf = false) {
        $labelStyle = 'font-size:' . ($forPdf ? '7.5pt' : '0.72rem')
            . ';text-transform:uppercase;letter-spacing:0.6px;color:#64748b;margin-bottom:4px;';
        $valueStyle = 'font-size:' . ($forPdf ? '9.5pt' : '0.9rem') . ';color:#0f172a;line-height:1.45;';
        $pending = [];
        if (trim((string) ($customer['name'] ?? '')) !== '') {
            $pending[] = ['label' => 'Customer', 'value' => admin_quote_h($customer['name']), 'value_style' => $valueStyle . 'font-weight:bold;'];
        }
        if (trim((string) ($customer['email'] ?? '')) !== '') {
            $pending[] = ['label' => 'Email', 'value' => admin_quote_h($customer['email']), 'value_style' => $valueStyle . 'word-break:break-all;'];
        }
        if (trim((string) ($customer['phone'] ?? '')) !== '') {
            $pending[] = ['label' => 'Phone', 'value' => admin_quote_h($customer['phone']), 'value_style' => $valueStyle];
        }
        if (trim((string) ($customer['address'] ?? '')) !== '') {
            $pending[] = ['label' => 'Address', 'value' => nl2br(admin_quote_h($customer['address'])), 'value_style' => $valueStyle];
        }
        if (empty($pending)) {
            return '';
        }

        $blocks = [];
        $lastIndex = count($pending) - 1;
        foreach ($pending as $i => $field) {
            $blocks[] = admin_quote_fill_template(admin_quote_template_pdf_customer_field(), [
                'margin_bottom' => $i < $lastIndex ? '10px' : '0',
                'label_style'   => $labelStyle,
                'value_style'   => $field['value_style'],
                'label'         => $field['label'],
                'value'         => $field['value'],
            ]);
        }

        return admin_quote_fill_template(admin_quote_template_pdf_customer_box(), [
            'brand_color' => admin_quote_h(admin_quote_pdf_brand_color()),
            'fields_html' => implode('', $blocks),
        ]);
    }
}

if (!function_exists('admin_build_quote_line_image_html')) {
    /**
     * Compact quote thumbnail (fixed size for PDF — dompdf ignores max-width/height:auto).
     */
    function admin_build_quote_line_image_html($imageUrl, $title) {
        $imageUrl = admin_normalize_quote_image_url($imageUrl);
        if ($imageUrl === '') {
            return '';
        }
        $tpl = admin_quote_load_pdf_template('items-row-image');
        if ($tpl !== '') {
            return admin_quote_fill_template($tpl, [
                'image_url' => admin_quote_h($imageUrl),
                'title'     => admin_quote_h($title),
            ]);
        }
        $width = 72;
        $height = 84;
        return admin_quote_fill_template(admin_quote_template_pdf_image(), [
            'image_url' => admin_quote_h($imageUrl),
            'title'     => admin_quote_h($title),
            'width'     => $width,
            'height'    => $height,
        ]);
    }
}

if (!function_exists('admin_build_quote_line_image_cell_html')) {
    /** Dedicated image table cell (or placeholder). */
    function admin_build_quote_line_image_cell_html($imageUrl, $title) {
        $imageHtml = admin_build_quote_line_image_html($imageUrl, $title);
        if ($imageHtml !== '') {
            return $imageHtml;
        }
        $tpl = admin_quote_load_pdf_template('items-row-image-placeholder');
        if ($tpl !== '') {
            return $tpl;
        }
        return admin_quote_template_pdf_image_placeholder();
    }
}

if (!function_exists('admin_build_quote_line_product_html')) {
    /** Product title and options (image is rendered in its own table column). */
    function admin_build_quote_line_product_html($title, $optionsHtml) {
        return admin_quote_fill_template(admin_quote_template_pdf_product(), [
            'title'        => admin_quote_h($title),
            'options_html' => $optionsHtml,
        ]);
    }
}

if (!function_exists('admin_build_folder_quote_table_html')) {
    /**
     * Line-item table for quote email / PDF.
     *
     * @param bool $forPdf Omit email heading; use PDF-friendly sizing.
     */
    function admin_build_folder_quote_table_html($projectName, $rowsHtml, $totalFormatted, $forPdf = false) {
        $projectName = admin_quote_h($projectName);
        $brandColor = admin_quote_h(admin_quote_pdf_brand_color());
        $heading = $forPdf
            ? ''
            : '<h2 style="margin:0 0 0.85rem 0;font-size:1.2rem;color:#0f172a;">Quote: ' . $projectName . '</h2>';

        $thStyle = 'text-align:left;padding:10px 12px;background-color:' . $brandColor . ';color:#ffffff;font-size:'
            . ($forPdf ? '8.5pt' : '0.82rem') . ';font-weight:bold;border:none;';
        $thCenter = str_replace('text-align:left', 'text-align:center', $thStyle);
        $thRight = str_replace('text-align:left', 'text-align:right', $thStyle);

        $footLabel = 'padding:12px;text-align:right;font-weight:bold;font-size:'
            . ($forPdf ? '10pt' : '0.95rem') . ';color:#0f172a;background-color:#f1f5f9;border-top:2px solid ' . $brandColor . ';';
        $footValue = str_replace('text-align:right;', 'text-align:right;', $footLabel);

        return admin_quote_fill_template(admin_quote_template_pdf_table(), [
            'heading'          => $heading,
            'th_style'         => $thStyle,
            'th_center'        => $thCenter,
            'th_right'         => $thRight,
            'rows_html'        => $rowsHtml,
            'foot_label'       => $footLabel,
            'foot_value'       => $footValue,
            'total_formatted'  => admin_quote_h($totalFormatted),
        ]);
    }
}

if (!function_exists('admin_build_folder_quote_summary')) {
    /**
     * @return array{html: string, text: string, total: float, total_formatted: string, item_count: int, project_name: string, image_urls: array, primary_image_url: string}
     */
    function admin_build_folder_quote_summary(array $folder, array $configs) {
        $projectName = trim((string) ($folder['name'] ?? 'Project'));
        $total = 0.0;
        $rowsHtml = '';
        $textLines = ['Quote for: ' . $projectName, ''];
        $imageUrls = [];

        $rowIndex = 0;
        foreach ($configs as $config) {
            $opts = json_decode((string) ($config['options_json'] ?? ''), true);
            if (!is_array($opts)) {
                $opts = [];
            }
            $configId = (int) ($config['id'] ?? 0);
            $title = admin_get_configuration_quote_title($opts, $config);
            $unitPrice = (float) ($config['computed_price'] ?? 0);
            $lineTotal = admin_get_order_total_price($opts, $unitPrice);
            $qty = max(1, (int) ($opts['_quantity'] ?? 1));
            $total += $lineTotal;
            $imageUrl = admin_config_quote_image_url($opts, $config);
            if ($imageUrl !== '') {
                $imageUrls[] = $imageUrl;
            }

            $descriptionText = admin_get_quote_description_text($opts, $config);
            if (admin_config_is_door_quote_product($opts, $config) && $descriptionText !== '') {
                $optionsHtml = admin_quote_fill_template(admin_quote_template_pdf_description(), [
                    'margin_top' => '4px',
                    'font_size'  => '8pt',
                    'text'       => nl2br(admin_quote_h($descriptionText)),
                ]);
            } else {
                $optionsHtml = admin_build_configuration_quote_description_html($opts, true);
            }
            $productCellHtml = admin_build_quote_line_product_html($title, $optionsHtml);
            $imageCellHtml = admin_build_quote_line_image_cell_html($imageUrl, $title);
            $rowBg = ($rowIndex % 2 === 1) ? 'background-color:#f8fafc;' : 'background-color:#ffffff;';
            $cell = 'padding:10px 12px;border-bottom:1px solid #e2e8f0;vertical-align:top;font-size:9pt;color:#1e293b;' . $rowBg;
            $brandColor = admin_quote_pdf_brand_color();

            $rowsHtml .= admin_quote_fill_template(admin_quote_template_pdf_table_row(), [
                'cell_style'  => $cell,
                'config_id'   => admin_quote_h($configId),
                'image_html'  => $imageCellHtml,
                'product_html'=> $productCellHtml,
                'qty'         => (int) $qty,
                'brand_color' => admin_quote_h($brandColor),
                'line_total'  => number_format($lineTotal, 2),
            ]);

            $textLines[] = '#' . $configId . ' — ' . $title . ' (qty ' . $qty . ') — $' . number_format($lineTotal, 2);
            if ($imageUrl !== '') {
                $textLines[] = '  Image: ' . $imageUrl;
            }
            if ($descriptionText !== '') {
                $textLines[] = '  ' . $descriptionText;
            }
            $textLines[] = '';
            $rowIndex++;
        }

        $totalFormatted = '$' . number_format($total, 2);
        $textLines[] = 'Total: ' . $totalFormatted;

        $html = admin_build_folder_quote_table_html($projectName, $rowsHtml, $totalFormatted, false);
        $htmlPdf = admin_build_quote_pdf_items_and_totals_html($configs);

        return [
            'html' => $html,
            'html_pdf' => $htmlPdf,
            'text' => implode("\n", $textLines),
            'total' => round($total, 2),
            'total_formatted' => $totalFormatted,
            'item_count' => count($configs),
            'project_name' => $projectName,
            'image_urls' => $imageUrls,
            'primary_image_url' => !empty($imageUrls) ? (string) $imageUrls[0] : '',
            'configs' => $configs,
        ];
    }
}

if (!function_exists('admin_resolve_hubspot_quote_email_id')) {
    function admin_resolve_hubspot_quote_email_id() {
        if (defined('HUBSPOT_QUOTE_EMAIL_ID') && (int) HUBSPOT_QUOTE_EMAIL_ID > 0) {
            return (int) HUBSPOT_QUOTE_EMAIL_ID;
        }
        $env = getenv('HUBSPOT_QUOTE_EMAIL_ID');
        if ($env !== false && (int) $env > 0) {
            return (int) $env;
        }
        return 0;
    }
}

if (!function_exists('admin_resolve_hubspot_quote_send_mode')) {
  /**
   * workflow (default) — patch contact properties; HubSpot workflow sends the Automated email.
   * transactional — set HUBSPOT_QUOTE_SEND_MODE to transactional; needs transactional-email scope + HUBSPOT_QUOTE_EMAIL_ID.
   */
    function admin_resolve_hubspot_quote_send_mode() {
        if (defined('HUBSPOT_QUOTE_SEND_MODE') && HUBSPOT_QUOTE_SEND_MODE !== '') {
            return strtolower(trim((string) HUBSPOT_QUOTE_SEND_MODE));
        }
        return 'workflow';
    }
}

if (!function_exists('admin_quote_pdf_autoload_paths')) {
    /**
     * Candidate autoload paths (admin may live in /admin/ with generate-pdf at site root).
     *
     * @return string[]
     */
    function admin_quote_pdf_autoload_paths() {
        $paths = [];
        if (defined('QUOTE_PDF_AUTOLOAD') && trim((string) QUOTE_PDF_AUTOLOAD) !== '') {
            $paths[] = trim((string) QUOTE_PDF_AUTOLOAD);
        }
        if (defined('GENERATE_PDF_DIR') && trim((string) GENERATE_PDF_DIR) !== '') {
            $paths[] = rtrim(str_replace('\\', '/', (string) GENERATE_PDF_DIR), '/') . '/vendor/autoload.php';
        }
        $paths[] = __DIR__ . '/generate-pdf/vendor/autoload.php';
        $paths[] = dirname(__DIR__) . '/generate-pdf/vendor/autoload.php';
        $paths[] = __DIR__ . '/vendor/autoload.php';
        $paths[] = dirname(__DIR__) . '/vendor/autoload.php';

        $unique = [];
        foreach ($paths as $path) {
            $path = str_replace('\\', '/', (string) $path);
            if ($path !== '' && !in_array($path, $unique, true)) {
                $unique[] = $path;
            }
        }
        return $unique;
    }
}

if (!function_exists('admin_load_quote_pdf_autoloader')) {
    function admin_load_quote_pdf_autoloader() {
        static $loaded = false;
        if ($loaded) {
            return class_exists('Dompdf\\Dompdf');
        }
        foreach (admin_quote_pdf_autoload_paths() as $path) {
            if (is_file($path)) {
                require_once $path;
                $loaded = true;
                return class_exists('Dompdf\\Dompdf');
            }
        }
        $loaded = true;
        return false;
    }
}

if (!function_exists('admin_quote_pdf_autoload_error')) {
    function admin_quote_pdf_autoload_error() {
        $expected = dirname(__DIR__) . '/generate-pdf/vendor/autoload.php';
        $tried = admin_quote_pdf_autoload_paths();
        return 'PDF library not found. Run composer install inside generate-pdf. '
            . 'Expected autoload at ' . $expected
            . ' (also checked ' . count($tried) . ' path(s)).';
    }
}

if (!function_exists('admin_quote_pdf_storage_dir')) {
    function admin_quote_pdf_storage_dir() {
        $dir = __DIR__ . '/storage/quotes';
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
        return $dir;
    }
}

if (!function_exists('admin_build_quote_document_html')) {
    /**
     * Customer-facing quote HTML used for PDF generation (matches email quote content).
     */
    function admin_build_quote_document_html(array $folder, $email, array $quote) {
        $folder = admin_load_folder_contact_for_display($folder);
        $customer = admin_collect_quote_customer_details($folder, $email);
        $configs = is_array($quote['configs'] ?? null) ? $quote['configs'] : [];
        $quoteHtml = trim((string) ($quote['html_pdf'] ?? $quote['html'] ?? ''));
        $logoUrl = admin_quote_h(admin_quote_pdf_logo_url());

        return admin_quote_fill_template(admin_quote_template_pdf_document(), [
            'logo_url'          => $logoUrl,
            'order_section'     => admin_build_quote_pdf_order_section_html($folder, $configs, $quote),
            'bill_ship_section' => admin_build_quote_pdf_bill_ship_html($customer),
            'quote_table'       => $quoteHtml,
            'generated_footer'  => admin_quote_h(admin_quote_pdf_generated_footer()),
        ]);
    }
}

if (!function_exists('admin_quote_pdf_filename')) {
    function admin_quote_pdf_filename(array $folder, array $quote) {
        $folderId = (int) ($folder['id'] ?? 0);
        $project = trim((string) ($quote['project_name'] ?? $folder['name'] ?? 'quote'));
        $slug = preg_replace('/[^a-zA-Z0-9_-]+/', '_', $project);
        $slug = trim((string) $slug, '_');
        if ($slug === '') {
            $slug = 'quote';
        }
        if (strlen($slug) > 48) {
            $slug = substr($slug, 0, 48);
        }
        return 'quote_' . ($folderId > 0 ? 'folder_' . $folderId . '_' : '') . $slug . '_' . gmdate('Ymd_His') . '.pdf';
    }
}

if (!function_exists('admin_generate_quote_pdf_file')) {
    /**
     * @return array{success: bool, path: string, error: string}
     */
    function admin_generate_quote_pdf_file(array $folder, $email, array $quote) {
        if (!admin_load_quote_pdf_autoloader()) {
            return [
                'success' => false,
                'path'    => '',
                'error'   => admin_quote_pdf_autoload_error(),
            ];
        }

        $html = admin_build_quote_document_html($folder, $email, $quote);
        $dir = admin_quote_pdf_storage_dir();
        $filename = admin_quote_pdf_filename($folder, $quote);
        $path = $dir . DIRECTORY_SEPARATOR . $filename;

        try {
            $dompdf = new Dompdf\Dompdf([
                'isRemoteEnabled' => true,
                'isHtml5ParserEnabled' => true,
            ]);
            $dompdf->loadHtml($html);
            $dompdf->setPaper('A4', 'portrait');
            $dompdf->render();
            $bytes = $dompdf->output();
            if ($bytes === '' || $bytes === false) {
                return ['success' => false, 'path' => '', 'error' => 'PDF generation returned empty output.'];
            }
            if (@file_put_contents($path, $bytes) === false) {
                return ['success' => false, 'path' => '', 'error' => 'Could not write PDF to storage/quotes.'];
            }
        } catch (Throwable $e) {
            return ['success' => false, 'path' => '', 'error' => 'PDF generation failed: ' . $e->getMessage()];
        }

        return ['success' => true, 'path' => $path, 'error' => ''];
    }
}

if (!function_exists('admin_create_quote_pdf_and_upload_to_shopify')) {
    /**
     * Generate quote PDF locally, upload to Shopify Files, return CDN URL for HubSpot email.
     *
     * @return array{success: bool, pdf_path: string, pdf_url: string, error: string}
     */
    function admin_create_quote_pdf_and_upload_to_shopify(array $folder, $email, array $quote) {
        $generated = admin_generate_quote_pdf_file($folder, $email, $quote);
        if (!$generated['success']) {
            return [
                'success'  => false,
                'pdf_path' => '',
                'pdf_url'  => '',
                'error'    => (string) ($generated['error'] ?? 'PDF generation failed.'),
            ];
        }
        $path = (string) ($generated['path'] ?? '');
        $filename = basename($path);
        $upload = admin_shopify_upload_pdf_file($path, $filename);
        if (empty($upload['success'])) {
            return [
                'success'  => false,
                'pdf_path' => $path,
                'pdf_url'  => '',
                'error'    => (string) ($upload['error'] ?? 'Shopify PDF upload failed.'),
            ];
        }
        return [
            'success'  => true,
            'pdf_path' => $path,
            'pdf_url'  => (string) ($upload['url'] ?? ''),
            'error'    => '',
        ];
    }
}

if (!function_exists('admin_hubspot_quote_contact_property_names')) {
    /**
     * HubSpot contact property internal names for quote email (create these in HubSpot → Settings → Properties → Contact).
     */
    function admin_hubspot_quote_contact_property_names() {
        return [
            'summary'    => defined('HUBSPOT_QUOTE_PROP_SUMMARY') ? (string) HUBSPOT_QUOTE_PROP_SUMMARY : 'admin_quote_summary',
            'total'      => defined('HUBSPOT_QUOTE_PROP_TOTAL') ? (string) HUBSPOT_QUOTE_PROP_TOTAL : 'admin_quote_total',
            'project'    => defined('HUBSPOT_QUOTE_PROP_PROJECT') ? (string) HUBSPOT_QUOTE_PROP_PROJECT : 'admin_quote_project_name',
            'trigger'    => defined('HUBSPOT_QUOTE_PROP_TRIGGER') ? (string) HUBSPOT_QUOTE_PROP_TRIGGER : 'admin_quote_send_trigger',
            'html'      => defined('HUBSPOT_QUOTE_PROP_HTML') ? (string) HUBSPOT_QUOTE_PROP_HTML : 'admin_quote_html',
            'image_url' => defined('HUBSPOT_QUOTE_PROP_IMAGE_URL') ? (string) HUBSPOT_QUOTE_PROP_IMAGE_URL : 'admin_quote_image_url',
            'pdf_url'   => defined('HUBSPOT_QUOTE_PROP_PDF_URL') ? (string) HUBSPOT_QUOTE_PROP_PDF_URL : 'admin_quote_pdf_url',
        ];
    }
}

if (!function_exists('admin_hubspot_quote_property_payload')) {
    /** Required HubSpot contact properties (must exist in HubSpot). */
    function admin_hubspot_quote_core_property_payload(array $folder, array $quote) {
        $names = admin_hubspot_quote_contact_property_names();
        return [
            $names['summary'] => (string) ($quote['text'] ?? ''),
            $names['total']   => (string) ($quote['total_formatted'] ?? ''),
            $names['project'] => trim((string) ($folder['name'] ?? '')),
            $names['trigger'] => gmdate('c'),
        ];
    }

    /** Optional HubSpot contact properties for HTML / images (create in HubSpot when ready). */
    function admin_hubspot_quote_extra_property_payload(array $folder, array $quote) {
        if (defined('HUBSPOT_QUOTE_SEND_EXTRA_PROPERTIES') && !HUBSPOT_QUOTE_SEND_EXTRA_PROPERTIES) {
            return [];
        }
        $names = admin_hubspot_quote_contact_property_names();
        $extra = [];
        $pdfUrl = trim((string) ($quote['pdf_url'] ?? ''));
        if ($pdfUrl !== '') {
            $extra[$names['pdf_url']] = $pdfUrl;
        }
        if (empty($extra)) {
            $extra = [
                $names['html']      => (string) ($quote['html'] ?? ''),
                $names['image_url'] => (string) ($quote['primary_image_url'] ?? ''),
            ];
        }
        return array_filter($extra, function ($value) {
            return trim((string) $value) !== '';
        });
    }

    /** @deprecated Use admin_hubspot_quote_core_property_payload + extra. */
    function admin_hubspot_quote_property_payload(array $folder, array $quote) {
        return array_merge(
            admin_hubspot_quote_core_property_payload($folder, $quote),
            admin_hubspot_quote_extra_property_payload($folder, $quote)
        );
    }
}

if (!function_exists('admin_hubspot_patch_crm_properties')) {
    function admin_hubspot_patch_crm_properties($objectType, $objectId, array $properties) {
        $objectType = trim((string) $objectType);
        $objectId = trim((string) $objectId);
        if ($objectType === '' || $objectId === '' || empty($properties)) {
            return ['ok' => false, 'error' => 'Missing CRM object or properties.'];
        }
        return admin_hubspot_json_request(
            'PATCH',
            '/crm/v3/objects/' . rawurlencode($objectType) . '/' . rawurlencode($objectId),
            ['properties' => $properties]
        );
    }
}

if (!function_exists('admin_hubspot_update_contact_quote_properties')) {
    function admin_hubspot_update_contact_quote_properties($contactId, array $folder, array $quote) {
        $contactId = trim((string) $contactId);
        if ($contactId === '') {
            return ['success' => false, 'error' => 'HubSpot contact id is required.'];
        }

        $core = admin_hubspot_quote_core_property_payload($folder, $quote);
        $res = admin_hubspot_patch_crm_properties('contacts', $contactId, $core);
        if (!$res['ok']) {
            $detail = is_string($res['error'] ?? null) ? $res['error'] : 'Failed to update HubSpot contact quote properties.';
            return ['success' => false, 'error' => $detail];
        }

        $extra = admin_hubspot_quote_extra_property_payload($folder, $quote);
        if (!empty($extra)) {
            $extraRes = admin_hubspot_patch_crm_properties('contacts', $contactId, $extra);
            if (!$extraRes['ok']) {
                $extraErr = is_string($extraRes['error'] ?? null) ? $extraRes['error'] : 'Failed to update optional HubSpot quote properties.';
                if (trim((string) ($quote['pdf_url'] ?? '')) !== '') {
                    return ['success' => false, 'error' => 'HubSpot PDF URL property update failed: ' . $extraErr];
                }
                return [
                    'success' => true,
                    'error'   => '',
                    'extra_properties_skipped' => true,
                    'extra_properties_error'   => $extraErr,
                ];
            }
        }

        return ['success' => true, 'error' => ''];
    }
}

if (!function_exists('admin_hubspot_patch_deal_quote_totals')) {
    function admin_hubspot_patch_deal_quote_totals($dealId, array $quote, array $folder = []) {
        $dealId = trim((string) $dealId);
        if ($dealId === '') {
            return ['success' => true, 'error' => ''];
        }
        if (trim((string) ($folder['name'] ?? '')) === '' && !empty($quote['project_name'])) {
            $folder['name'] = (string) $quote['project_name'];
        }
        $props = array_merge(
            admin_hubspot_quote_core_property_payload($folder, $quote),
            admin_hubspot_quote_extra_property_payload($folder, $quote)
        );
        $props['amount'] = (string) ($quote['total'] ?? 0);
        $res = admin_hubspot_patch_crm_properties('deals', $dealId, $props);
        if (!$res['ok']) {
            $coreOnly = admin_hubspot_quote_core_property_payload($folder, $quote);
            $coreOnly['amount'] = (string) ($quote['total'] ?? 0);
            $res2 = admin_hubspot_patch_crm_properties('deals', $dealId, $coreOnly);
            if (!$res2['ok']) {
                $res3 = admin_hubspot_patch_crm_properties('deals', $dealId, [
                    'amount' => (string) ($quote['total'] ?? 0),
                ]);
                if (!$res3['ok']) {
                    return ['success' => false, 'error' => is_string($res3['error'] ?? null) ? $res3['error'] : 'Failed to update HubSpot deal.'];
                }
            }
        }
        return ['success' => true, 'error' => ''];
    }
}

if (!function_exists('admin_hubspot_send_quote_transactional_email')) {
    /**
     * Send quote via HubSpot Marketing transactional single-send API.
     * Requires a transactional email template in HubSpot (HUBSPOT_QUOTE_EMAIL_ID).
     * Template tokens: {{ custom.quote_html }}, {{ custom.quote_text }}, {{ custom.project_name }}, {{ custom.quote_total }}
     */
    function admin_hubspot_send_quote_transactional_email($toEmail, array $contactProps, array $customProps) {
        $toEmail = trim((string) $toEmail);
        if ($toEmail === '') {
            return ['success' => false, 'error' => 'Recipient email is required.'];
        }
        if (!admin_load_hubspot_config()) {
            return ['success' => false, 'error' => 'HubSpot is not configured.'];
        }
        $emailId = admin_resolve_hubspot_quote_email_id();
        if ($emailId <= 0) {
            return [
                'success' => false,
                'error' => 'Set HUBSPOT_QUOTE_EMAIL_ID in config.php to your HubSpot transactional email template ID.',
            ];
        }

        $body = [
            'emailId' => $emailId,
            'message' => ['to' => $toEmail],
            'contactProperties' => [],
            'customProperties' => $customProps,
        ];
        $first = trim((string) ($contactProps['firstname'] ?? $contactProps['customer_first_name'] ?? ''));
        $last = trim((string) ($contactProps['lastname'] ?? $contactProps['customer_last_name'] ?? ''));
        if ($first !== '') {
            $body['contactProperties']['firstname'] = $first;
        }
        if ($last !== '') {
            $body['contactProperties']['lastname'] = $last;
        }

        $res = admin_hubspot_json_request('POST', '/marketing/v3/transactional/single-email/send', $body);
        if (!$res['ok']) {
            $detail = is_string($res['error'] ?? null) ? $res['error'] : 'HubSpot email send failed.';
            return ['success' => false, 'error' => $detail];
        }
        return ['success' => true, 'error' => ''];
    }
}

if (!function_exists('admin_send_folder_quote_via_hubspot')) {
    /**
     * Sync selected configs to HubSpot deal, then email the quote to the customer.
     *
     * @return array{success: bool, error: string, quote?: array}
     */
    function admin_send_folder_quote_via_hubspot(PDO $pdo, array $folder, array $configIds) {
        $folderId = (int) ($folder['id'] ?? 0);
        $configIds = array_values(array_unique(array_filter(array_map('intval', $configIds))));
        if ($folderId <= 0 || empty($configIds)) {
            return ['success' => false, 'error' => 'Select at least one project to email.'];
        }

        $folder = admin_load_folder_contact_for_display($folder);
        $email = trim((string) ($folder['customer_email'] ?? ''));
        if ($email === '') {
            return ['success' => false, 'error' => 'This project has no customer email. Add one in the contact panel first.'];
        }

        $placeholders = implode(',', array_fill(0, count($configIds), '?'));
        $stmt = $pdo->prepare(
            "SELECT * FROM door_configurations WHERE folder_id = ? AND id IN ($placeholders) ORDER BY id ASC"
        );
        $stmt->execute(array_merge([$folderId], $configIds));
        $configs = $stmt->fetchAll(PDO::FETCH_ASSOC);
        if (empty($configs)) {
            return ['success' => false, 'error' => 'No matching projects found in this list.'];
        }

        $hsContact = admin_upsert_hubspot_contact($email, [
            'customer_first_name' => (string) ($folder['customer_first_name'] ?? ''),
            'customer_last_name'  => (string) ($folder['customer_last_name'] ?? ''),
            'customer_phone'      => (string) ($folder['customer_phone'] ?? ''),
            'billing_address'     => (string) ($folder['billing_address'] ?? ''),
        ]);
        if (!$hsContact['success']) {
            return ['success' => false, 'error' => 'HubSpot contact sync failed: ' . $hsContact['error']];
        }

        $syncErrors = [];
        $dealId = '';
        foreach ($configs as $config) {
            $sync = admin_sync_configuration_to_hubspot_deal($config, $folder);
            if (!$sync['success']) {
                $syncErrors[] = '#' . (int) $config['id'] . ': ' . $sync['error'];
            } elseif (!empty($sync['deal_id'])) {
                $dealId = (string) $sync['deal_id'];
            }
        }
        if (!empty($syncErrors)) {
            return ['success' => false, 'error' => 'HubSpot deal sync failed: ' . implode('; ', $syncErrors)];
        }

        $quote = admin_build_folder_quote_summary($folder, $configs);

        $pdfResult = admin_create_quote_pdf_and_upload_to_shopify($folder, $email, $quote);
        if (!$pdfResult['success']) {
            return [
                'success' => false,
                'error'   => 'Quote PDF failed: ' . (string) ($pdfResult['error'] ?? 'unknown error'),
                'quote'   => $quote,
            ];
        }
        $quote['pdf_url'] = (string) ($pdfResult['pdf_url'] ?? '');
        $quote['pdf_path'] = (string) ($pdfResult['pdf_path'] ?? '');

        $contactId = (string) ($hsContact['contact_id'] ?? '');
        $quoteProps = admin_hubspot_update_contact_quote_properties($contactId, $folder, $quote);
        if (!$quoteProps['success']) {
            return ['success' => false, 'error' => $quoteProps['error']];
        }

        if ($dealId !== '') {
            $dealPatch = admin_hubspot_patch_deal_quote_totals($dealId, $quote, $folder);
            if (!$dealPatch['success']) {
                return ['success' => false, 'error' => $dealPatch['error']];
            }
        }

        $sendMode = admin_resolve_hubspot_quote_send_mode();
        if ($sendMode === 'transactional') {
            $send = admin_hubspot_send_quote_transactional_email($email, [
                'customer_first_name' => (string) ($folder['customer_first_name'] ?? ''),
                'customer_last_name'  => (string) ($folder['customer_last_name'] ?? ''),
            ], [
                'quote_html'   => $quote['html'],
                'quote_text'   => $quote['text'],
                'project_name' => (string) ($folder['name'] ?? ''),
                'quote_total'  => $quote['total_formatted'],
                'quote_pdf_url' => (string) ($quote['pdf_url'] ?? ''),
            ]);
            if (!$send['success']) {
                return ['success' => false, 'error' => $send['error']];
            }
        }

        admin_log_quote_email_preview($folder, $email, $quote, $sendMode);

        return [
            'success' => true,
            'error'   => '',
            'quote'   => $quote,
            'send_mode' => $sendMode,
            'pdf_url' => (string) ($quote['pdf_url'] ?? ''),
            'pdf_path' => (string) ($quote['pdf_path'] ?? ''),
            'extra_properties_skipped' => !empty($quoteProps['extra_properties_skipped']),
        ];
    }
}

if (!function_exists('admin_quote_preview_log_dir')) {
    function admin_quote_preview_log_dir() {
        $dir = __DIR__ . '/logs';
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
        return $dir;
    }
}

if (!function_exists('admin_quote_preview_paths')) {
    function admin_quote_preview_paths($folderId = 0) {
        $dir = admin_quote_preview_log_dir();
        $folderId = (int) $folderId;
        return [
            'dir'         => $dir,
            'latest_json' => $dir . '/quote_email_preview_latest.json',
            'latest_html' => $dir . '/quote_email_preview_latest.html',
            'log'         => $dir . '/quote_email_preview.log',
            'folder_html' => $folderId > 0 ? $dir . '/quote_email_preview_folder_' . $folderId . '.html' : '',
        ];
    }
}

if (!function_exists('admin_build_quote_email_preview_html')) {
    function admin_build_quote_email_preview_html(array $folder, $email, array $quote, $sendMode = 'workflow') {
        $firstName = trim((string) ($folder['customer_first_name'] ?? ''));
        $greeting = $firstName !== '' ? 'Hi ' . htmlspecialchars($firstName, ENT_QUOTES, 'UTF-8') . ',' : 'Hello,';
        $project = htmlspecialchars((string) ($quote['project_name'] ?? $folder['name'] ?? ''), ENT_QUOTES, 'UTF-8');
        $total = htmlspecialchars((string) ($quote['total_formatted'] ?? ''), ENT_QUOTES, 'UTF-8');
        $to = htmlspecialchars((string) $email, ENT_QUOTES, 'UTF-8');
        $summaryText = htmlspecialchars((string) ($quote['text'] ?? ''), ENT_QUOTES, 'UTF-8');
        $quoteHtml = (string) ($quote['html'] ?? '');
        $sentAt = gmdate('Y-m-d H:i:s') . ' UTC';
        $mode = htmlspecialchars((string) $sendMode, ENT_QUOTES, 'UTF-8');

        return '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
            . '<meta name="viewport" content="width=device-width, initial-scale=1">'
            . '<title>Quote email preview</title>'
            . '<style>'
            . 'body{font-family:Arial,sans-serif;background:#f1f5f9;margin:0;padding:1.5rem;color:#0f172a;}'
            . '.wrap{max-width:720px;margin:0 auto;}'
            . '.meta{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:1rem 1.25rem;margin-bottom:1rem;font-size:0.85rem;}'
            . '.meta dt{font-weight:600;color:#475569;margin-top:0.5rem;}'
            . '.meta dd{margin:0.15rem 0 0 0;}'
            . '.email{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:1.5rem;box-shadow:0 1px 3px rgba(15,23,42,.06);}'
            . '.email h1{font-size:1.15rem;margin:0 0 1rem 0;}'
            . '.plain{margin-top:1.25rem;padding:1rem;background:#f8fafc;border:1px dashed #cbd5e1;border-radius:8px;white-space:pre-wrap;font-size:0.82rem;line-height:1.5;}'
            . '</style></head><body><div class="wrap">'
            . '<div class="meta"><strong>Quote email preview (sandbox log)</strong>'
            . '<dl><dt>To</dt><dd>' . $to . '</dd>'
            . '<dt>Project</dt><dd>' . $project . '</dd>'
            . '<dt>Total</dt><dd>' . $total . '</dd>'
            . '<dt>Send mode</dt><dd>' . $mode . '</dd>'
            . '<dt>Logged at</dt><dd>' . $sentAt . '</dd></dl></div>'
            . '<div class="email"><h1>Your quote</h1><p>' . $greeting . '</p>'
            . '<p><strong>Project:</strong> ' . $project . '<br><strong>Total:</strong> ' . $total . '</p>'
            . $quoteHtml
            . '<div class="plain"><strong>Plain text (admin_quote_summary):</strong>' . "\n" . $summaryText . '</div>'
            . '</div></div></body></html>';
    }
}

if (!function_exists('admin_log_quote_email_preview')) {
    /**
     * Write quote email preview to logs/ for sandbox testing.
     *
     * @return array{ok: bool, html_path: string, log_path: string, json_path: string}
     */
    function admin_log_quote_email_preview(array $folder, $email, array $quote, $sendMode = 'workflow') {
        $folderId = (int) ($folder['id'] ?? 0);
        $paths = admin_quote_preview_paths($folderId);
        $trigger = gmdate('c');

        $meta = [
            'time'         => $trigger,
            'folder_id'    => $folderId,
            'folder_name'  => (string) ($folder['name'] ?? ''),
            'to_email'     => (string) $email,
            'send_mode'    => (string) $sendMode,
            'quote_total'  => (string) ($quote['total_formatted'] ?? ''),
            'item_count'   => (int) ($quote['item_count'] ?? 0),
            'contact_properties' => admin_hubspot_quote_property_payload($folder, $quote),
            'quote_text' => (string) ($quote['text'] ?? ''),
            'quote_html' => (string) ($quote['html'] ?? ''),
            'quote_pdf_url' => (string) ($quote['pdf_url'] ?? ''),
            'image_urls' => is_array($quote['image_urls'] ?? null) ? $quote['image_urls'] : [],
        ];

        $html = admin_build_quote_email_preview_html($folder, $email, $quote, $sendMode);
        @file_put_contents($paths['latest_html'], $html);
        if ($paths['folder_html'] !== '') {
            @file_put_contents($paths['folder_html'], $html);
        }
        @file_put_contents($paths['latest_json'], json_encode($meta, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

        $logLine = $trigger . ' | folder=' . $folderId
            . ' | to=' . (string) $email
            . ' | total=' . (string) ($quote['total_formatted'] ?? '')
            . ' | items=' . (int) ($quote['item_count'] ?? 0)
            . ' | preview=quote_email_preview_folder_' . $folderId . '.html'
            . "\n";
        @file_put_contents($paths['log'], $logLine, FILE_APPEND | LOCK_EX);

        return [
            'ok'        => true,
            'html_path' => $paths['folder_html'] !== '' ? $paths['folder_html'] : $paths['latest_html'],
            'log_path'  => $paths['log'],
            'json_path' => $paths['latest_json'],
        ];
    }
}

if (!function_exists('admin_quote_preview_url')) {
    function admin_quote_preview_url($folderId, $adminWebBase = '') {
        $folderId = (int) $folderId;
        $base = rtrim((string) $adminWebBase, '/');
        $path = ($base !== '' ? $base . '/' : '') . 'quote_email_preview.php?folder_id=' . $folderId;
        return $path;
    }
}
