<?php
namespace DS_Plugin;

if (!defined('ABSPATH')) { exit; }

class Admin {
    private static function d_zones($type) {
        if ($type === 'tshirt') {
            return wp_json_encode([
                [ 'id' => 'front_full', 'label' => 'Front', 'width_mm' => 210, 'height_mm' => 297 ],
                [ 'id' => 'back_full', 'label' => 'Back', 'width_mm' => 210, 'height_mm' => 297 ],
            ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
        return wp_json_encode([[ 'id' => 'wrap', 'label' => 'Main', 'width_mm' => 200, 'height_mm' => 90 ]], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    private static function d_preview($type) {
        if ($type === 'tshirt') {
            return wp_json_encode(['model' => 'tshirt'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
        if ($type === 'none') {
            return wp_json_encode(['model' => 'none'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
        $circ = ($type === 'bottle') ? 250.0 : 230.0;
        $radius = $circ / (3.141592653589793 * 2.0);
        $height = ($type === 'bottle') ? 180.0 : 95.0;
        return wp_json_encode([
            'model' => 'mug_like',
            'circumference_mm' => $circ,
            'model_radius_mm' => $radius,
            'model_height_mm' => $height,
            'model_wall_thickness_mm' => ($type === 'bottle' ? 2.6 : 3.2),
            'blank_handle_mm' => ($type === 'bottle' ? 38.0 : 45.0),
            'canvas_start_offset_mm' => ($type === 'bottle' ? 19.0 : 22.5),
            'handle_theta_rad' => -1.5707963267948966,
        ], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }

    private static function parse_preview($raw, $type) {
        $json = trim((string) $raw);
        if ($json === '') $json = self::d_preview($type);
        $obj = json_decode($json, true);
        if (!is_array($obj)) {
            $json = self::d_preview($type);
            $obj = json_decode($json, true);
            if (!is_array($obj)) $obj = [];
        }
        return [$json, $obj];
    }

    private static function n($v, $f) {
        $x = is_numeric($v) ? (float) $v : (float) $f;
        if (is_nan($x) || is_infinite($x)) $x = (float) $f;
        return $x;
    }

    private static function norm_area($a, $dw, $dh) {
        $w = max(0.05, min(1.0, self::n($a['w_pct'] ?? 0.3, 0.3)));
        $h = max(0.05, min(1.0, self::n($a['h_pct'] ?? 0.3, 0.3)));
        $x = max(0.0, min(1.0 - $w, self::n($a['x_pct'] ?? 0.35, 0.35)));
        $y = max(0.0, min(1.0 - $h, self::n($a['y_pct'] ?? 0.35, 0.35)));
        $excludes = [];
        if (isset($a['excludes']) && is_array($a['excludes'])) {
            foreach ($a['excludes'] as $eid) {
                $k = sanitize_key((string) $eid);
                if ($k !== '') $excludes[] = $k;
            }
        }
        $excludes = array_values(array_unique($excludes));
        $allow_subcanvas = !empty($a['allow_subcanvas']);
        $portrait_w = max(1, self::n($a['portrait_width_mm'] ?? min($dw, $dh), min($dw, $dh)));
        $portrait_h = max(1, self::n($a['portrait_height_mm'] ?? max($dw, $dh), max($dw, $dh)));
        $landscape_w = max(1, self::n($a['landscape_width_mm'] ?? $portrait_h, $portrait_h));
        $landscape_h = max(1, self::n($a['landscape_height_mm'] ?? $portrait_w, $portrait_w));
        $area_id = sanitize_key($a['id'] ?? wp_unique_id('area_'));
        if ($area_id === '') $area_id = sanitize_key(wp_unique_id('area_'));
        $zone_id = sanitize_key($a['zone_id'] ?? $a['id'] ?? $area_id);
        if ($zone_id === '') $zone_id = $area_id;
        return [
            'id' => $area_id,
            'zone_id' => $zone_id,
            'label' => sanitize_text_field($a['label'] ?? 'Area'),
            'width_mm' => max(1, self::n($a['width_mm'] ?? $dw, $dw)),
            'height_mm' => max(1, self::n($a['height_mm'] ?? $dh, $dh)),
            'x_pct' => $x,
            'y_pct' => $y,
            'w_pct' => $w,
            'h_pct' => $h,
            'excludes' => $excludes,
            'allow_subcanvas' => $allow_subcanvas ? 1 : 0,
            'portrait_width_mm' => $portrait_w,
            'portrait_height_mm' => $portrait_h,
            'landscape_width_mm' => $landscape_w,
            'landscape_height_mm' => $landscape_h,
        ];
    }

    private static function unique_key($value, $prefix, &$seen) {
        $base = sanitize_key((string) $value);
        if ($base === '') $base = sanitize_key(wp_unique_id($prefix));
        if ($base === '') $base = sanitize_key($prefix . '_' . wp_unique_id());
        $key = $base;
        $i = 2;
        while (isset($seen[$key])) {
            $key = $base . '_' . $i;
            $i++;
        }
        $seen[$key] = 1;
        return $key;
    }

    private static function norm_mockups($list, $dw, $dh) {
        $out = [];
        $seen_mockup_ids = [];
        $seen_area_ids = [];
        if (is_array($list)) {
            foreach ($list as $m) {
                if (!is_array($m)) continue;
                $side = sanitize_key($m['side'] ?? 'custom');
                if (!in_array($side, ['custom', 'front', 'back', 'left', 'right'], true)) $side = 'custom';
                $areas = [];
                $seen_zone_ids_in_mockup = [];
                if (!empty($m['areas']) && is_array($m['areas'])) {
                    foreach ($m['areas'] as $a) {
                        $area = self::norm_area((array) $a, $dw, $dh);
                        $area['id'] = self::unique_key($area['id'] ?? '', 'area_', $seen_area_ids);
                        $zone_source = $area['zone_id'] ?? $area['id'] ?? '';
                        $area['zone_id'] = self::unique_key($zone_source, 'zone_', $seen_zone_ids_in_mockup);
                        if ($area['zone_id'] === '') $area['zone_id'] = $area['id'];
                        $areas[] = $area;
                    }
                }
                if (!$areas) {
                    $area = self::norm_area([], $dw, $dh);
                    $area['id'] = self::unique_key($area['id'] ?? '', 'area_', $seen_area_ids);
                    $zone_source = $area['zone_id'] ?? $area['id'] ?? '';
                    $area['zone_id'] = self::unique_key($zone_source, 'zone_', $seen_zone_ids_in_mockup);
                    if ($area['zone_id'] === '') $area['zone_id'] = $area['id'];
                    $areas[] = $area;
                }
                $out[] = [
                    'id' => self::unique_key($m['id'] ?? wp_unique_id('mockup_'), 'mockup_', $seen_mockup_ids),
                    'name' => sanitize_text_field($m['name'] ?? 'Mockup'),
                    'side' => $side,
                    'image_url' => esc_url_raw($m['image_url'] ?? ''),
                    'areas' => $areas,
                ];
            }
        }
        if (!$out) {
            $out[] = [
                'id' => 'mockup_1',
                'name' => 'Mockup 1',
                'side' => 'custom',
                'image_url' => '',
                'areas' => [self::norm_area([], $dw, $dh)],
            ];
        }
        return $out;
    }

    private static function legacy_map($mockups) {
        $u = ['front' => '', 'back' => '', 'left' => '', 'right' => ''];
        $a = ['front' => [], 'back' => [], 'left' => [], 'right' => []];
        foreach (['front','back','left','right'] as $s) {
            foreach ($mockups as $m) {
                if (($m['side'] ?? '') !== $s) continue;
                $u[$s] = (string) ($m['image_url'] ?? '');
                if (!empty($m['areas'][0])) {
                    $ar = $m['areas'][0];
                    $a[$s] = [
                        'x_pct' => (float) ($ar['x_pct'] ?? 0.35),
                        'y_pct' => (float) ($ar['y_pct'] ?? 0.35),
                        'w_pct' => (float) ($ar['w_pct'] ?? 0.3),
                        'h_pct' => (float) ($ar['h_pct'] ?? 0.3),
                    ];
                }
                break;
            }
        }
        if ($u['front'] === '') {
            foreach ($mockups as $m) {
                if (empty($m['image_url'])) continue;
                $u['front'] = (string) ($m['image_url'] ?? '');
                if (!empty($m['areas'][0])) {
                    $ar = $m['areas'][0];
                    $a['front'] = [
                        'x_pct' => (float) ($ar['x_pct'] ?? 0.35),
                        'y_pct' => (float) ($ar['y_pct'] ?? 0.35),
                        'w_pct' => (float) ($ar['w_pct'] ?? 0.3),
                        'h_pct' => (float) ($ar['h_pct'] ?? 0.3),
                    ];
                }
                break;
            }
        }
        return ['mockup_2d' => $u, 'mockup_2d_areas' => $a];
    }

    private static function zones_from_mockups($mockups, $dw, $dh) {
        $zones = [];
        $seen = [];
        foreach ($mockups as $m) {
            $side = sanitize_key($m['side'] ?? 'custom');
            $areas = !empty($m['areas']) && is_array($m['areas']) ? $m['areas'] : [];
            foreach ($areas as $area) {
                if (!is_array($area)) continue;
                $id = sanitize_key($area['zone_id'] ?? $area['id'] ?? '');
                if ($id === '') $id = sanitize_key(wp_unique_id('zone_'));
                if ($id === '') continue;
                if (isset($seen[$id])) continue;
                $seen[$id] = 1;
                $zones[] = [
                    'id' => $id,
                    'zone_id' => $id,
                    'label' => sanitize_text_field($area['label'] ?? $id),
                    'width_mm' => max(1, self::n($area['width_mm'] ?? $dw, $dw)),
                    'height_mm' => max(1, self::n($area['height_mm'] ?? $dh, $dh)),
                    'side' => $side,
                    'excludes' => isset($area['excludes']) && is_array($area['excludes']) ? array_values(array_unique(array_map('sanitize_key', $area['excludes']))) : [],
                    'allow_subcanvas' => !empty($area['allow_subcanvas']) ? 1 : 0,
                    'portrait_width_mm' => max(1, self::n($area['portrait_width_mm'] ?? min($dw, $dh), min($dw, $dh))),
                    'portrait_height_mm' => max(1, self::n($area['portrait_height_mm'] ?? max($dw, $dh), max($dw, $dh))),
                    'landscape_width_mm' => max(1, self::n($area['landscape_width_mm'] ?? max($dw, $dh), max($dw, $dh))),
                    'landscape_height_mm' => max(1, self::n($area['landscape_height_mm'] ?? min($dw, $dh), min($dw, $dh))),
                ];
            }
        }
        if (!$zones) {
            $zones[] = [
                'id' => 'wrap',
                'zone_id' => 'wrap',
                'label' => 'Main',
                'width_mm' => max(1, self::n($dw, 200)),
                'height_mm' => max(1, self::n($dh, 90)),
                'side' => 'custom',
            ];
        }
        return $zones;
    }

    public static function init() {
        add_action('add_meta_boxes', [__CLASS__, 'add_meta_boxes']);
        add_action('save_post_ds_template', [__CLASS__, 'save_template_meta']);
        add_action('admin_enqueue_scripts', [__CLASS__, 'enqueue_admin_assets']);
        add_filter('mime_types', [__CLASS__, 'allow_model_upload_mimes']);
        add_filter('upload_mimes', [__CLASS__, 'allow_model_upload_mimes']);
        add_filter('wp_check_filetype_and_ext', [__CLASS__, 'normalize_model_filetype'], 10, 5);
        add_filter('wp_prevent_unsupported_mime_type_uploads', [__CLASS__, 'allow_unsupported_model_mimes'], 10, 2);
    }

    public static function allow_model_upload_mimes($mimes) {
        if (!current_user_can('upload_files')) return $mimes;
        if (!is_array($mimes)) $mimes = [];
        $mimes['glb'] = 'model/gltf-binary';
        $mimes['gltf'] = 'model/gltf+json';
        $mimes['stl'] = 'model/stl';
        return $mimes;
    }

    public static function normalize_model_filetype($data, $file, $filename, $mimes, $real_mime = '') {
        if (!current_user_can('upload_files')) return $data;
        $ext = strtolower((string) pathinfo((string) $filename, PATHINFO_EXTENSION));
        if (!in_array($ext, ['glb', 'gltf', 'stl'], true)) return $data;

        $type_map = [
            'glb' => 'model/gltf-binary',
            'gltf' => 'model/gltf+json',
            'stl' => 'model/stl',
        ];
        $forced = isset($type_map[$ext]) ? $type_map[$ext] : 'application/octet-stream';

        if (!is_array($data)) $data = [];
        $data['ext'] = $ext;
        $data['type'] = $forced;
        $data['proper_filename'] = false;
        return $data;
    }

    public static function allow_unsupported_model_mimes($prevent, $mime_type) {
        $mime = strtolower(trim((string) $mime_type));
        if (in_array($mime, ['model/gltf-binary', 'model/gltf+json', 'model/stl'], true)) {
            return false;
        }
        return $prevent;
    }

    public static function enqueue_admin_assets($hook) {
        if (!in_array($hook, ['post.php', 'post-new.php'], true)) return;
        $screen = get_current_screen();
        if (!$screen || $screen->post_type !== 'ds_template') return;

        wp_enqueue_media();
        wp_register_style('ds-admin-inline', false, [], '1.3.0');
        wp_enqueue_style('ds-admin-inline');
        wp_add_inline_style('ds-admin-inline', '.ds-mb{display:grid;grid-template-columns:210px 1fr;gap:12px}.ds-box{border:1px solid #d0d3d6;border-radius:6px;background:#fff;padding:8px}.ds-list{display:flex;flex-direction:column;gap:6px}.ds-mi{padding:6px 8px;border:1px solid #cbd5e1;border-radius:4px;background:#f8fafc;cursor:pointer}.ds-mi.active{background:#dbeafe;border-color:#2563eb}.ds-row{display:flex;gap:6px;flex-wrap:wrap}.ds-prev{position:relative;max-width:560px;min-height:220px;border:1px solid #d0d3d6;background:#f8fafc;padding:6px;overflow:hidden}.ds-prev.has{background:#fff}.ds-img{max-width:100%;height:auto;display:block}.ds-ar{position:absolute;border:2px dashed #0f766e;background:rgba(15,118,110,.15);cursor:move;box-sizing:border-box}.ds-ar.active{border-color:#1d4ed8;background:rgba(29,78,216,.16)}.ds-h{position:absolute;right:-6px;bottom:-6px;width:14px;height:14px;background:#1d4ed8;border:2px solid #fff;cursor:nwse-resize}.ds-tag{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.ds-tag button{border:1px solid #cbd5e1;background:#fff;border-radius:999px;padding:3px 8px;cursor:pointer}.ds-tag button.active{border-color:#2563eb;background:#dbeafe}.ds-master-pill{display:inline-block;margin-left:6px;padding:1px 5px;border-radius:999px;font-size:10px;line-height:1.4;background:#16a34a;color:#fff;vertical-align:middle}.ds-master-note{margin-top:6px;font-size:12px;color:#4b5563}.ds-grid{display:grid;grid-template-columns:repeat(4,minmax(90px,1fr));gap:8px;margin-top:8px}.ds-grid label{display:flex;flex-direction:column;gap:4px;font-size:12px}.ds-grid input,.ds-grid select{width:100%}.ds-hide{display:none}');
        wp_add_inline_style('ds-admin-inline', '.ds-3d-calib{margin-top:12px;padding:10px;border:1px solid #d0d7e2;border-radius:8px;background:#f8fafc}.ds-3d-calib-grid{display:grid;grid-template-columns:minmax(220px,280px) 1fr;gap:12px;align-items:start}.ds-3d-pad{position:relative;width:240px;height:240px;border:1px dashed #94a3b8;border-radius:8px;background:linear-gradient(180deg,#ffffff,#eef3f8);touch-action:none;overflow:hidden}.ds-3d-pad::before,.ds-3d-pad::after{content:\"\";position:absolute;background:#c8d3e1}.ds-3d-pad::before{left:50%;top:0;bottom:0;width:1px;transform:translateX(-.5px)}.ds-3d-pad::after{top:50%;left:0;right:0;height:1px;transform:translateY(-.5px)}.ds-3d-cup-fixed{position:absolute;left:50%;top:50%;width:58px;height:58px;border-radius:50%;transform:translate(-50%,-50%);background:radial-gradient(circle at 32% 28%, #ffffff 0 20%, #e5ebf2 45%, #ccd6e3 100%);border:1px solid #7c8ca3;box-shadow:inset 0 0 0 2px rgba(255,255,255,.5), 0 2px 4px rgba(2,8,23,.18);pointer-events:none}.ds-3d-print-arc{position:absolute;left:50%;top:50%;width:96px;height:96px;transform:translate(-50%,-50%) rotate(var(--arc-rot,0deg));border-radius:50%;background:conic-gradient(from 0deg, rgba(10,127,115,.88) 0deg var(--arc-len,300deg), rgba(148,163,184,.22) var(--arc-len,300deg) 360deg);-webkit-mask:radial-gradient(farthest-side, transparent calc(100% - 9px), #000 calc(100% - 8px));mask:radial-gradient(farthest-side, transparent calc(100% - 9px), #000 calc(100% - 8px));box-shadow:0 0 0 1px rgba(51,65,85,.2);cursor:grab}.ds-3d-print-arc.is-dragging{cursor:grabbing}.ds-3d-readout{font-size:12px;color:#334155;margin-top:6px}.ds-3d-sliders{display:grid;grid-template-columns:repeat(2,minmax(160px,1fr));gap:10px}.ds-3d-sliders label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:#334155}.ds-3d-sliders input[type=range]{width:100%}.ds-zones3d-row{margin-top:10px;padding:10px;border:1px solid #d7dee9;border-radius:8px;background:#fff}.ds-zones3d-row h4{margin:0 0 8px;font-size:13px}.ds-zones3d-grid{display:grid;grid-template-columns:repeat(4,minmax(120px,1fr));gap:8px}.ds-zones3d-grid label{display:flex;flex-direction:column;gap:4px;font-size:12px}.ds-zones3d-grid input{width:100%}.ds-3d-live{margin-top:12px;padding:10px;border:1px solid #d0d7e2;border-radius:8px;background:#fff}.ds-3d-live-stage{position:relative;width:100%;max-width:420px;height:280px;border:1px solid #d7dee9;border-radius:8px;background:linear-gradient(180deg,#f8fbff,#e9eff7);overflow:hidden}.ds-3d-live-stage canvas{display:block;width:100%;height:100%}.ds-3d-live-meta{margin-top:6px;font-size:12px;color:#475569}.ds-advanced-json-row td{padding-top:2px}.ds-advanced-json-note{margin-top:6px}');

        $three_vendor_url = esc_url_raw(DS_PLUGIN_URL . 'assets/vendor/three.min.js');
        $gltf_vendor_url = esc_url_raw(DS_PLUGIN_URL . 'assets/vendor/GLTFLoader.min.js');
        $js = <<<'JS'
(function($){
  function c(v,a,b){ return Math.max(a, Math.min(b, v)); }
  function n(v,f){ var x=parseFloat(v); return isFinite(x)?x:f; }
  function id(p){ return p+'_'+Math.random().toString(36).slice(2,8); }

  var S={m:[],mi:0,ai:0,d:null,link:false,adv:false};

  function T(){
    return { w: Math.max(1,n($('#ds_width_mm').val(),200)), h: Math.max(1,n($('#ds_height_mm').val(),90)) };
  }

  function imgSize(){
    var im = $('[data-r="img"]')[0];
    if (!im || !im.clientWidth || !im.clientHeight) return {w:0,h:0};
    return { w: im.clientWidth, h: im.clientHeight };
  }

  function lockAspect(a, iw, ih, driver){
    var ratio = Math.max(0.001, n(a.width_mm, 200) / Math.max(1, n(a.height_mm, 90))); // mm ratio

    a.w_pct = c(n(a.w_pct, 0.3), 0.02, 1);
    a.h_pct = c(n(a.h_pct, 0.3), 0.02, 1);

    if (iw > 0 && ih > 0) {
      if (driver === 'h') {
        a.w_pct = (a.h_pct * ih * ratio) / iw;
      } else {
        a.h_pct = (a.w_pct * iw) / (ih * ratio);
      }

      if (!isFinite(a.w_pct) || a.w_pct <= 0) a.w_pct = 0.3;
      if (!isFinite(a.h_pct) || a.h_pct <= 0) a.h_pct = 0.3;

      if (a.w_pct > 1) {
        a.w_pct = 1;
        a.h_pct = (a.w_pct * iw) / (ih * ratio);
      }
      if (a.h_pct > 1) {
        a.h_pct = 1;
        a.w_pct = (a.h_pct * ih * ratio) / iw;
      }

      a.w_pct = c(a.w_pct, 0.02, 1);
      a.h_pct = c(a.h_pct, 0.02, 1);
    }

    a.x_pct = c(n(a.x_pct,0.35), 0, 1-a.w_pct);
    a.y_pct = c(n(a.y_pct,0.35), 0, 1-a.h_pct);
    return a;
  }

  function A(o, driver){
    var t=T();
    o=o||{};
    o.id=String(o.id||id('area')).replace(/[^a-zA-Z0-9_\-]/g,'');
    o.zone_id=String(o.zone_id||o.id||id('zone')).replace(/[^a-zA-Z0-9_\-]/g,'');
    o.label=String(o.label||'Area');
    o.width_mm=Math.max(1,n(o.width_mm,t.w));
    o.height_mm=Math.max(1,n(o.height_mm,t.h));
    o.w_pct=c(n(o.w_pct,.3),.02,1);
    o.h_pct=c(n(o.h_pct,.3),.02,1);
    o.x_pct=n(o.x_pct,.35);
    o.y_pct=n(o.y_pct,.35);
    o.excludes = Array.isArray(o.excludes) ? o.excludes.map(function(v){ return String(v||'').replace(/[^a-zA-Z0-9_\-]/g,''); }).filter(Boolean) : [];
    o.excludes = o.excludes.filter(function(v){ return v !== o.id; }).filter(function(v, i, arr){ return arr.indexOf(v) === i; });
    o.allow_subcanvas = !!o.allow_subcanvas;
    o.portrait_width_mm = Math.max(1, n(o.portrait_width_mm, Math.min(o.width_mm, o.height_mm)));
    o.portrait_height_mm = Math.max(1, n(o.portrait_height_mm, Math.max(o.width_mm, o.height_mm)));
    o.landscape_width_mm = Math.max(1, n(o.landscape_width_mm, o.portrait_height_mm));
    o.landscape_height_mm = Math.max(1, n(o.landscape_height_mm, o.portrait_width_mm));

    var s = imgSize();
    return lockAspect(o, s.w, s.h, driver || 'w');
  }

  function M(o){
    o=o||{};
    return {
      id:String(o.id||id('mockup')).replace(/[^a-zA-Z0-9_\-]/g,''),
      name:String(o.name||'Mockup'),
      side:['custom','front','back','left','right'].includes(o.side)?o.side:'custom',
      image_url:String(o.image_url||''),
      areas:(Array.isArray(o.areas)&&o.areas.length?o.areas:[A()]).map(function(x){ return A(x,'w'); })
    };
  }

  function sanitizeKey(v){
    return String(v || '').toLowerCase().replace(/[^a-z0-9_\-]/g,'');
  }

  function E(){
    if(!S.m.length){ S.m=[M({name:'Mockup 1'})]; S.mi=0; S.ai=0; }
    S.mi=c(S.mi,0,S.m.length-1);
    if(!S.m[S.mi].areas.length) S.m[S.mi].areas=[A()];
    S.ai=c(S.ai,0,S.m[S.mi].areas.length-1);
  }

  function G(){ E(); return {m:S.m[S.mi],a:S.m[S.mi].areas[S.ai]}; }
  function H(){
    $('#ds_mockups_json').val(JSON.stringify(S.m));
    $('#ds_link_mockup_scale').val(S.link ? '1' : '0');
  }

  function isMasterAreaAt(mockupIndex, areaIndex){
    if(S.link){
      return mockupIndex === 0 && areaIndex === 0;
    }
    return areaIndex === 0;
  }

  function esc(s){
    return String(s||'').replace(/[&<>"']/g,function(ch){
      return({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'})[ch];
    });
  }

  function rl(){
    var h='';
    S.m.forEach(function(m,i){ h+='<div class="ds-mi '+(i===S.mi?'active':'')+'" data-mi="'+i+'">'+esc(m.name||('Mockup '+(i+1)))+'</div>'; });
    $('[data-r="ml"]').html(h);
  }

  function ra(){
    var m=G().m,h='';
    m.areas.forEach(function(a,i){
      var isMaster = isMasterAreaAt(S.mi, i);
      var pill = isMaster ? '<span class="ds-master-pill">Master</span>' : '';
      h+='<button type="button" class="'+(i===S.ai?'active':'')+'" data-ai="'+i+'">'+esc(a.label||('Area '+(i+1)))+pill+'</button>';
    });
    $('[data-r="al"]').html(h);
    var note = S.link ? 'Master scale: Mockup 1 / Area 1 (global).' : 'Master scale: Area 1 of current mockup.';
    $('[data-r="master-note"]').text(note);
  }

  function re(){
    var g=G(),m=g.m,a=g.a;
    $('[data-i="mn"]').val(m.name);
    $('[data-i="ms"]').val(m.side);
    $('[data-i="mu"]').val(m.image_url);
    $('[data-i="al"]').val(a.label);
    $('[data-i="awm"]').val(a.width_mm);
    $('[data-i="ahm"]').val(a.height_mm);
    $('[data-i="ax"]').val((a.x_pct*100).toFixed(2));
    $('[data-i="ay"]').val((a.y_pct*100).toFixed(2));
    $('[data-i="aw"]').val((a.w_pct*100).toFixed(2));
    $('[data-i="ah"]').val((a.h_pct*100).toFixed(2));
    $('[data-i="aor"]').prop('checked', !!a.allow_subcanvas);
    $('[data-i="apw"]').val(a.portrait_width_mm || '');
    $('[data-i="aph"]').val(a.portrait_height_mm || '');
    $('[data-i="alw"]').val(a.landscape_width_mm || '');
    $('[data-i="alh"]').val(a.landscape_height_mm || '');
    refreshExcludeOptions();
    var showSub = !!a.allow_subcanvas;
    $('[data-r="subdims"]').toggle(showSub);
  }

  function refreshExcludeOptions(){
    var g = G(), m = g.m, a = g.a;
    var excludes = Array.isArray(a.excludes) ? a.excludes.slice() : [];
    var html = '';
    (m.areas || []).forEach(function(other){
      if(!other || !other.id || other.id === a.id) return;
      var selected = excludes.indexOf(other.id) !== -1
        || excludes.indexOf(String(other.label || '')) !== -1; // legacy/manual compatibility
      html += '<option value=\"'+esc(other.id)+'\"'+(selected?' selected':'')+'>'+esc(other.label || other.id)+'</option>';
    });
    $('[data-i=\"aexm\"]').html(html);
  }

  function rp(){
    var g=G(),m=g.m,$img=$('[data-r="img"]'),$w=$('[data-r="pv"]');
    $img.attr('src',m.image_url||'');
    if(m.image_url){$w.addClass('has');$img.show();}else{$w.removeClass('has');$img.hide();}
    var h='';
    m.areas.forEach(function(a,i){ h+='<div class="ds-ar '+(i===S.ai?'active':'')+'" data-ai="'+i+'"><span class="ds-h" data-z="1" data-ai="'+i+'"></span></div>'; });
    $('[data-r="ov"]').html(h);
    px();
  }

  function px(){
    var g=G(),m=g.m,img=$('[data-r="img"]')[0],$w=$('[data-r="pv"]');
    if(!img||!img.clientWidth||!img.clientHeight||!m.image_url){ $('[data-r="ov"] .ds-ar').hide(); return; }
    var pxl=n($w.css('padding-left'),0), pyt=n($w.css('padding-top'),0);
    $('[data-r="ov"] .ds-ar').show().each(function(){
      var i=parseInt($(this).attr('data-ai'),10),a=m.areas[i];
      if(!a) return;
      m.areas[i] = isMasterAreaAt(S.mi, i) ? A(a,'w') : scaleAreaByFirst(m, a);
      a = m.areas[i];
      $(this).css({left:(pxl+a.x_pct*img.clientWidth)+'px', top:(pyt+a.y_pct*img.clientHeight)+'px', width:(a.w_pct*img.clientWidth)+'px', height:(a.h_pct*img.clientHeight)+'px'});
    });
  }

  function R(){ E(); rl(); $('[data-a="delm"]').prop('disabled', S.m.length<=1); ra(); re(); rp(); H(); }

  function AfromRef(ref, label){
    var t = T();
    var base = A({ label: label || 'Area', width_mm: t.w, height_mm: t.h }, 'w');
    if(!ref){ return base; }
    var rw = Math.max(1, n(ref.width_mm, t.w));
    var rh = Math.max(1, n(ref.height_mm, t.h));
    var sw = n(ref.w_pct, 0.3) / rw; // width % per mm from calibrated area
    var sh = n(ref.h_pct, 0.3) / rh; // height % per mm from calibrated area
    base.w_pct = c(base.width_mm * sw, 0.02, 1);
    base.h_pct = c(base.height_mm * sh, 0.02, 1);
    base = A(base, 'w');
    return base;
  }

  function getScaleMaster(m){
    var source = m;
    if (S.link && Array.isArray(S.m) && S.m.length && S.m[0] && Array.isArray(S.m[0].areas) && S.m[0].areas.length) {
      source = S.m[0];
    }
    if(!source || !Array.isArray(source.areas) || !source.areas.length) return null;
    return source.areas[0];
  }

  function mmScaleFromMaster(m){
    var f = getScaleMaster(m);
    if(!f) return null;
    var fw = Math.max(1, n(f.width_mm, 1));
    var fh = Math.max(1, n(f.height_mm, 1));
    var sw = Math.max(0.0001, n(f.w_pct, 0.3) / fw); // width % per mm
    var sh = Math.max(0.0001, n(f.h_pct, 0.3) / fh); // height % per mm
    return { sw: sw, sh: sh };
  }

  function scaleAreaByFirst(m, area){
    var sc = mmScaleFromMaster(m);
    if(!sc){ return A(area, 'w'); }
    var a = $.extend({}, area);
    a.w_pct = c(Math.max(1, n(a.width_mm, 1)) * sc.sw, 0.02, 1);
    a.h_pct = c(Math.max(1, n(a.height_mm, 1)) * sc.sh, 0.02, 1);
    return A(a, 'w');
  }

  function reflowAreasFromFirst(m){
    if(!m || !Array.isArray(m.areas) || m.areas.length < 2) return;
    for(var i=1;i<m.areas.length;i++){
      m.areas[i] = scaleAreaByFirst(m, m.areas[i]);
    }
  }

  function reflowAllMockupsFromMaster(){
    if(!S.link || !Array.isArray(S.m) || !S.m.length) return;
    for(var mi=0; mi<S.m.length; mi++){
      if(!S.m[mi] || !Array.isArray(S.m[mi].areas)) continue;
      for(var ai=0; ai<S.m[mi].areas.length; ai++){
        if(mi===0 && ai===0) continue; // keep master unchanged
        S.m[mi].areas[ai] = scaleAreaByFirst(S.m[mi], S.m[mi].areas[ai]);
      }
    }
  }

  function setA(){
    var g=G(),a=g.a;
    a.label=String($('[data-i="al"]').val()||'Area');
    a.width_mm=Math.max(1,n($('[data-i="awm"]').val(),a.width_mm));
    a.height_mm=Math.max(1,n($('[data-i="ahm"]').val(),a.height_mm));
    a.x_pct=c(n($('[data-i="ax"]').val(),a.x_pct*100)/100,0,1);
    a.y_pct=c(n($('[data-i="ay"]').val(),a.y_pct*100)/100,0,1);
    a.w_pct=c(n($('[data-i="aw"]').val(),a.w_pct*100)/100,.02,1);
    a.h_pct=c(n($('[data-i="ah"]').val(),a.h_pct*100)/100,.02,1);
    a.excludes = ($('[data-i="aexm"]').val() || [])
      .map(function(v){ return String(v || '').replace(/[^a-zA-Z0-9_\-]/g,''); })
      .filter(Boolean)
      .filter(function(v){ return v !== (a.id || ''); })
      .filter(function(v, i, arr){ return arr.indexOf(v) === i; });
    a.allow_subcanvas = $('[data-i="aor"]').is(':checked');
    a.portrait_width_mm = Math.max(1, n($('[data-i="apw"]').val(), a.portrait_width_mm || Math.min(a.width_mm, a.height_mm)));
    a.portrait_height_mm = Math.max(1, n($('[data-i="aph"]').val(), a.portrait_height_mm || Math.max(a.width_mm, a.height_mm)));
    a.landscape_width_mm = Math.max(1, n($('[data-i="alw"]').val(), a.landscape_width_mm || a.portrait_height_mm));
    a.landscape_height_mm = Math.max(1, n($('[data-i="alh"]').val(), a.landscape_height_mm || a.portrait_width_mm));
    if(isMasterAreaAt(S.mi, S.ai)){
      g.m.areas[S.ai] = A(a,'w');
      reflowAreasFromFirst(g.m);
      reflowAllMockupsFromMaster();
    } else {
      g.m.areas[S.ai] = scaleAreaByFirst(g.m, a);
    }
    R();
  }

  function sd(e,m,i){
    var g=G();
    if(i!==S.ai){S.ai=i;R();g=G();}
    if(m === 'resize' && !isMasterAreaAt(S.mi, i)){ return; }
    var im=$('[data-r="img"]')[0];
    if(!im||!im.clientWidth||!im.clientHeight) return;
    e.preventDefault();
    var oe=e.originalEvent&&e.originalEvent.touches?e.originalEvent.touches[0]:e;
    S.d={m:m,sx:oe.clientX,sy:oe.clientY,w:im.clientWidth,h:im.clientHeight,a:$.extend({},g.a)};
    $(document).on('mousemove.dsm touchmove.dsm',md);
    $(document).on('mouseup.dsm touchend.dsm touchcancel.dsm',ed);
  }

  function md(e){
    if(!S.d) return;
    var g=G(), oe=e.originalEvent&&e.originalEvent.touches?e.originalEvent.touches[0]:e;
    var dx=(oe.clientX-S.d.sx)/S.d.w, dy=(oe.clientY-S.d.sy)/S.d.h;
    var a=$.extend({},S.d.a);
    if(S.d.m==='move'){
      a.x_pct += dx; a.y_pct += dy;
      g.m.areas[S.ai] = isMasterAreaAt(S.mi, S.ai) ? A(a,'w') : scaleAreaByFirst(g.m, a);
    } else {
      var useH = Math.abs(dy) > Math.abs(dx);
      if(useH){ a.h_pct += dy; g.m.areas[S.ai]=A(a,'h'); }
      else { a.w_pct += dx; g.m.areas[S.ai]=A(a,'w'); }
      reflowAreasFromFirst(g.m);
      reflowAllMockupsFromMaster();
    }
    re(); px(); H();
  }

  function ed(){ S.d=null; $(document).off('.dsm'); }

  $(function(){
    // Avoid native HTML5 validation blocking WordPress publish flow on this CPT
    // (hidden metabox fields can otherwise trigger wp-post-new-reload loops).
    var postForm = document.getElementById('post');
    if (postForm) {
      postForm.setAttribute('novalidate', 'novalidate');
      postForm.noValidate = true;
      postForm.addEventListener('submit', function(){
        this.noValidate = true;
      }, true);
    }

    try{ S.m=(JSON.parse($('[data-r="src"]').text()||'[]')||[]).map(M); }catch(e){ S.m=[]; }
    S.link = $('[data-i="link-scale"]').is(':checked');

    var f=null;
    $('[data-a="up"]').on('click',function(e){
      e.preventDefault();
      if(!f){
        f=wp.media({title:'Select mockup image',button:{text:'Use this image'},library:{type:'image'},multiple:false});
        f.on('select',function(){ var g=G(),a=f.state().get('selection').first().toJSON(); g.m.image_url=String(a.url||''); R(); });
      }
      f.open();
    });

    $('[data-a="addm"]').on('click',function(){ S.m.push(M({name:'Mockup '+(S.m.length+1)})); S.mi=S.m.length-1; S.ai=0; R(); });
    $('[data-a="delm"]').on('click',function(){
      if(S.m.length<=1) return;
      if(!window.confirm('Remove selected mockup?')) return;
      S.m.splice(S.mi,1);
      S.mi=c(S.mi,0,S.m.length-1);
      S.ai=0;
      R();
    });
    $('[data-r="ml"]').on('click','.ds-mi',function(){ S.mi=parseInt($(this).attr('data-mi'),10)||0; S.ai=0; R(); });
    $('[data-a="clr"]').on('click',function(){ G().m.image_url=''; R(); });
    $('[data-a="adda"]').on('click',function(){
      var g=G();
      var ref = (g.m.areas && g.m.areas[0]) ? $.extend({}, g.m.areas[0]) : null;
      g.m.areas.push(AfromRef(ref, 'Area '+(g.m.areas.length+1)));
      g.m.areas[g.m.areas.length-1] = scaleAreaByFirst(g.m, g.m.areas[g.m.areas.length-1]);
      S.ai=g.m.areas.length-1;
      R();
    });
    $('[data-a="dela"]').on('click',function(){ var g=G(); if(g.m.areas.length<=1)return; g.m.areas.splice(S.ai,1); S.ai=c(S.ai,0,g.m.areas.length-1); R(); });
    $('[data-r="al"]').on('click','button',function(){ S.ai=parseInt($(this).attr('data-ai'),10)||0; R(); });

    $('[data-r="ov"]').on('mousedown touchstart','.ds-ar',function(e){ var i=parseInt($(this).attr('data-ai'),10)||0; if($(e.target).closest('[data-z]').length)return; sd(e,'move',i); });
    $('[data-r="ov"]').on('mousedown touchstart','[data-z]',function(e){ var i=parseInt($(this).attr('data-ai'),10)||0; sd(e,'resize',i); });

    $('[data-r="img"]').on('load',function(){ px(); H(); });
    $(window).on('resize',px);

    $('[data-i="mn"]').on('input',function(){ G().m.name=String($(this).val()||'Mockup'); rl(); H(); });
    $('[data-i="ms"]').on('change',function(){ G().m.side=String($(this).val()||'custom'); H(); });
    $('[data-i="link-scale"]').on('change', function(){
      S.link = $(this).is(':checked');
      reflowAllMockupsFromMaster();
      R();
    });
    $('[data-i="mu"]').on('input change',function(){ G().m.image_url=String($(this).val()||''); rp(); H(); });

    $('[data-i^="a"]').on('input change',setA);
    $('#ds_width_mm,#ds_height_mm').on('input change',R);

    function sync3dZonesJsonFromFields(){
      var is3d = String($('#ds_preview_mode').val() || '2d') === '3d';
      if(!is3d) return false;
      var zid = sanitizeKey($('#ds_zone_canvas_id').val() || 'wrap');
      if(!zid) zid = 'wrap';
      var zlabel = String($('#ds_zone_canvas_label').val() || 'Main').trim() || 'Main';
      var zw = Math.max(1, n($('#ds_zone_canvas_width_mm').val(), n($('#ds_width_mm').val(), 200)));
      var zh = Math.max(1, n($('#ds_zone_canvas_height_mm').val(), n($('#ds_height_mm').val(), 90)));
      var z = [{ id: zid, zone_id: zid, label: zlabel, width_mm: zw, height_mm: zh }];
      $('#ds_zones_json').val(JSON.stringify(z, null, 2));
      // Keep legacy width/height in sync for code paths that still use cfg.width_mm/height_mm.
      $('#ds_width_mm').val(String(zw));
      $('#ds_height_mm').val(String(zh));
      return true;
    }

    function bind3dCalibrator(){
      var $pad = $('#ds_3d_pad');
      var $arc = $('#ds_3d_print_arc');
      if(!$pad.length || !$arc.length) return;
      var dragging = false;
      var baseCanvasW = Math.max(1, n($('#ds_zone_canvas_width_mm').val(), n($('#ds_width_mm').val(), 200)));
      var baseCanvasH = Math.max(1, n($('#ds_zone_canvas_height_mm').val(), n($('#ds_height_mm').val(), 90)));
      function parseCfg(){
        var raw = $('#ds_preview_config_json').val() || '';
        try {
          var j = JSON.parse(raw);
          return (j && typeof j === 'object') ? j : {};
        } catch (e) { return {}; }
      }
      function read(){
        return {
          x: n($('#ds_model_offset_x').val(), 0),
          z: n($('#ds_model_offset_z').val(), 0),
          rot: n($('#ds_model_rotation_y').val(), 0)
        };
      }
      function write(v){
        $('#ds_model_offset_x').val(v.x.toFixed(2)).trigger('change');
        $('#ds_model_offset_z').val(v.z.toFixed(2)).trigger('change');
      }
      function draw(){
        var w = $pad.innerWidth(), h = $pad.innerHeight();
        var v = read();
        var arcX = -v.x;
        var arcZ = -v.z;
        var px = c((arcX + 1) * 0.5, 0, 1) * w;
        var py = c((1 - (arcZ + 1) * 0.5), 0, 1) * h;

        var cfg = parseCfg();
        var radiusMm = Math.max(1, n($('#ds_model_radius_mm').val(), n(cfg.model_radius_mm, 230 / (Math.PI * 2))));
        var circ = Math.max(1, radiusMm * Math.PI * 2);
        var blank = n($('#ds_blank_handle_mm').val(), n(cfg.blank_handle_mm, -1));
        if (blank < 0) {
          var templateW = Math.max(1, n($('#ds_zone_canvas_width_mm').val(), n($('#ds_width_mm').val(), 200)));
          blank = Math.max(0, circ - templateW);
        }
        var printMm = Math.max(1, circ - blank);
        var arcDeg = c((printMm / circ) * 360, 20, 359);
        var arcRotDeg = (v.rot * (180 / Math.PI));
        var arcScale = c(n($('#ds_canvas_scale_rng').val(), 1), 0.5, 1.8);
        var arcSize = 96 * arcScale;
        $arc.css({
          left: px + 'px',
          top: py + 'px',
          width: arcSize + 'px',
          height: arcSize + 'px',
          '--arc-len': arcDeg.toFixed(2) + 'deg',
          '--arc-rot': arcRotDeg.toFixed(2) + 'deg'
        });
        $('#ds_3d_pad_readout').text(
          'Offset X: ' + v.x.toFixed(2) +
          ' | Offset Z: ' + v.z.toFixed(2) +
          ' | Arc: ' + arcDeg.toFixed(1) + ' deg' +
          ' | Scale: ' + arcScale.toFixed(2)
        );
      }
      function pick(ev){
        var r = $pad[0].getBoundingClientRect();
        var oe = ev.originalEvent && ev.originalEvent.touches ? ev.originalEvent.touches[0] : ev;
        var rx = c((oe.clientX - r.left) / Math.max(1, r.width), 0, 1);
        var ry = c((oe.clientY - r.top) / Math.max(1, r.height), 0, 1);
        var arcX = (rx * 2) - 1;
        var arcZ = ((1 - ry) * 2) - 1;
        // Arc moves independently, cup remains centered. Model offset gets inverse relation.
        write({ x: -arcX, z: -arcZ });
        draw();
      }
      $arc.on('mousedown touchstart', function(e){ dragging = true; $arc.addClass('is-dragging'); pick(e); e.preventDefault(); });
      $(document).on('mousemove.ds3dpad touchmove.ds3dpad', function(e){ if(!dragging) return; pick(e); e.preventDefault(); });
      $(document).on('mouseup.ds3dpad touchend.ds3dpad touchcancel.ds3dpad', function(){ dragging = false; $arc.removeClass('is-dragging'); });
      $('#ds_model_offset_x,#ds_model_offset_z').on('input change', draw);
      $('#ds_model_scale_rng').on('input change', function(){ $('#ds_model_scale').val(n(this.value,1).toFixed(2)); });
      $('#ds_model_rot_rng').on('input change', function(){ $('#ds_model_rotation_y').val(n(this.value,0).toFixed(2)).trigger('change'); });
      $('#ds_canvas_scale_rng').on('input change', function(){
        var s = c(n(this.value, 1), 0.5, 1.8);
        $('#ds_zone_canvas_width_mm').val((baseCanvasW * s).toFixed(1)).trigger('change');
        $('#ds_zone_canvas_height_mm').val((baseCanvasH * s).toFixed(1)).trigger('change');
        draw();
      });
      $('#ds_model_scale').on('input change', function(){ $('#ds_model_scale_rng').val(String(n(this.value,1))); });
      $('#ds_model_rotation_y').on('input change', function(){ $('#ds_model_rot_rng').val(String(n(this.value,0))); draw(); });
      $('#ds_zone_canvas_width_mm,#ds_zone_canvas_height_mm,#ds_width_mm,#ds_preview_config_json,#ds_model_radius_mm,#ds_blank_handle_mm').on('input change', function(){
        baseCanvasW = Math.max(1, n($('#ds_zone_canvas_width_mm').val(), baseCanvasW));
        baseCanvasH = Math.max(1, n($('#ds_zone_canvas_height_mm').val(), baseCanvasH));
        draw();
      });
      draw();
    }

    function bind3dLivePreview(){
      var host = document.getElementById('ds_3d_live_stage');
      var meta = document.getElementById('ds_3d_live_meta');
      if(!host) return;
      var S3 = {
        ready:false, renderer:null, scene:null, camera:null, root:null, raf:0, loaderReady:false, drag:false, lx:0, ly:0, yaw:0.28, pitch:-0.18, roll:0, loadToken:0
      };
      function setMeta(t){ if(meta) meta.textContent = String(t || ''); }
      function parseCfg(){
        var raw = $('#ds_preview_config_json').val() || '';
        try { var j = JSON.parse(raw); return (j && typeof j === 'object') ? j : {}; } catch(e){ return {}; }
      }
      function readParams(){
        var cfg = parseCfg();
        var zoneW = Math.max(1, n($('#ds_zone_canvas_width_mm').val(), n($('#ds_width_mm').val(), 200)));
        var zoneH = Math.max(1, n($('#ds_zone_canvas_height_mm').val(), n($('#ds_height_mm').val(), 90)));
        var radiusMm = Math.max(1, n($('#ds_model_radius_mm').val(), n(cfg.model_radius_mm, 230 / (Math.PI * 2))));
        var modelHeightMm = Math.max(1, n($('#ds_model_height_mm').val(), n(cfg.model_height_mm, zoneH)));
        var circ = Math.max(1, radiusMm * Math.PI * 2);
        var wallMm = Math.max(0.5, n($('#ds_model_wall_thickness_mm').val(), n(cfg.model_wall_thickness_mm, 3.2)));
        var blank = n($('#ds_blank_handle_mm').val(), n(cfg.blank_handle_mm, -1));
        if(blank < 0) blank = Math.max(0, circ - zoneW);
        var printMm = Math.max(1, circ - blank);
        var startOffset = n($('#ds_canvas_start_offset_mm').val(), n(cfg.canvas_start_offset_mm, -1));
        if(startOffset < 0) startOffset = Math.max(0, blank / 2);
        var handleTheta = n(cfg.handle_theta_rad, -Math.PI / 2);
        return {
          type3d: String($('#ds_preview_3d_type').val() || 'mug').toLowerCase(),
          modelUrl: String($('#ds_model_url').val() || '').trim(),
          scale: Math.max(0.01, n($('#ds_model_scale').val(), 1)),
          rotY: n($('#ds_model_rotation_y').val(), 0),
          offX: n($('#ds_model_offset_x').val(), 0),
          offZ: n($('#ds_model_offset_z').val(), 0),
          radiusMm: radiusMm, modelHeightMm: modelHeightMm, wallMm: wallMm,
          zoneW: zoneW, zoneH: zoneH, circ: circ, blank: blank, printMm: printMm, startOffset: startOffset, handleTheta: handleTheta
        };
      }
      function loadScriptOnce(src, key, cb){
        if(window[key]) { cb(true); return; }
        var tag = document.querySelector('script[data-ds-admin="'+key+'"]');
        if(tag){ tag.addEventListener('load', function(){ cb(true); }); tag.addEventListener('error', function(){ cb(false); }); return; }
        var s = document.createElement('script');
        s.src = src;
        s.async = false;
        s.setAttribute('data-ds-admin', key);
        s.onload = function(){ window[key] = 1; cb(true); };
        s.onerror = function(){ cb(false); };
        document.head.appendChild(s);
      }
      function ensureDeps(done){
        if(window.THREE && window.THREE.GLTFLoader){ done(true); return; }
        loadScriptOnce('__DS_ADMIN_THREE_URL__', '__DS_ADMIN_THREE__', function(ok1){
          if(!ok1 || !window.THREE){ done(false); return; }
          if(window.THREE.GLTFLoader){ done(true); return; }
          loadScriptOnce('__DS_ADMIN_GLTF_URL__', '__DS_ADMIN_GLTF__', function(ok2){
            if(window.THREE && !window.THREE.GLTFLoader && window.GLTFLoader) window.THREE.GLTFLoader = window.GLTFLoader;
            done(!!(ok2 && window.THREE && window.THREE.GLTFLoader));
          });
        });
      }
      function resize(){
        if(!S3.renderer || !S3.camera) return;
        var w = Math.max(220, host.clientWidth || 220);
        var h = Math.max(180, host.clientHeight || 180);
        S3.renderer.setSize(w, h, false);
        S3.camera.aspect = w / h;
        S3.camera.updateProjectionMatrix();
      }
      function clearRoot(){
        if(!S3.root) return;
        while(S3.root.children.length){
          var c = S3.root.children.pop();
          try {
            if(c.geometry) c.geometry.dispose();
            if(c.material && c.material.dispose) c.material.dispose();
          } catch(e){}
        }
      }
      function initScene(){
        if(S3.ready) return true;
        if(!window.THREE) return false;
        var THREE = window.THREE;
        var r;
        try { r = new THREE.WebGLRenderer({antialias:true, alpha:true}); } catch(e){ return false; }
        host.innerHTML = '';
        host.appendChild(r.domElement);
        host.style.touchAction = 'none';
        var scene = new THREE.Scene();
        var cam = new THREE.PerspectiveCamera(34, 1, 0.1, 100);
        cam.position.set(0, -0.55, 4.28);
        cam.lookAt(0, 0.2, 0);
        scene.add(new THREE.HemisphereLight(0xffffff, 0xc4ccd8, 1.05));
        var key = new THREE.DirectionalLight(0xffffff, 1.1); key.position.set(2.2, 3.4, 2.1); scene.add(key);
        var rim = new THREE.DirectionalLight(0xbfd6ff, 0.7); rim.position.set(-2.2, 1.2, -2.1); scene.add(rim);
        var root = new THREE.Group(); root.scale.setScalar(0.76); root.rotation.order = 'YXZ'; scene.add(root);
        S3.renderer = r; S3.scene = scene; S3.camera = cam; S3.root = root; S3.ready = true;
        host.addEventListener('pointerdown', function(e){ S3.drag=true; S3.lx=e.clientX; S3.ly=e.clientY; host.setPointerCapture && host.setPointerCapture(e.pointerId); e.preventDefault(); });
        host.addEventListener('pointermove', function(e){
          if(!S3.drag) return;
          var dx = e.clientX - S3.lx, dy = e.clientY - S3.ly; S3.lx = e.clientX; S3.ly = e.clientY;
          if (e.shiftKey) {
            S3.roll = c(S3.roll + (dx * 0.008), -Math.PI, Math.PI);
          } else {
            S3.yaw += dx * 0.008;
            S3.pitch = c(S3.pitch + dy * 0.008, -1.35, 1.35);
          }
          e.preventDefault();
        });
        var endDrag = function(){ S3.drag=false; };
        host.addEventListener('pointerup', endDrag);
        host.addEventListener('pointercancel', endDrag);
        resize();
        function tick(){
          if(!S3.ready) return;
          root.rotation.y = S3.yaw;
          root.rotation.x = S3.pitch;
          root.rotation.z = S3.roll;
          r.render(scene, cam);
          S3.raf = window.requestAnimationFrame(tick);
        }
        tick();
        return true;
      }
      function buildLabel(p){
        var THREE = window.THREE;
        var mmToUnit = 1/48;
        var outerR = Math.max(0.01, p.radiusMm) * mmToUnit;
        var bodyHeight = Math.max(1, p.modelHeightMm) * mmToUnit;
        var thetaPerMm = (Math.PI * 2) / p.circ;
        var thetaLength = p.printMm * thetaPerMm;
        var thetaStart = (p.handleTheta - (p.startOffset * thetaPerMm)) - thetaLength;
        var mat = new THREE.MeshStandardMaterial({ color: 0x0a7f73, roughness:0.45, metalness:0.04, transparent:true, opacity:0.75 });
        var label = new THREE.Mesh(
          new THREE.CylinderGeometry(outerR + 0.006, outerR + 0.006, bodyHeight * 0.95, 128, 1, true, thetaStart, thetaLength),
          mat
        );
        S3.root.add(label);
        return { outerR: outerR, bodyHeight: bodyHeight };
      }
      function buildProceduralCup(outerR, bodyHeight, wallMm, isBottle){
        var THREE = window.THREE;
        var mmToUnit = 1/48;
        var wall = Math.max(0.002, Math.abs(wallMm || 3.2) * mmToUnit);
        var innerR = Math.max(outerR * 0.45, outerR - wall);
        var mat = new THREE.MeshStandardMaterial({ color: 0xf8f8fb, roughness:0.62, metalness:0.05 });
        var body = new THREE.Mesh(new THREE.CylinderGeometry(outerR, innerR, bodyHeight, 96, 1, true), mat);
        S3.root.add(body);
        var topCap = new THREE.Mesh(new THREE.CylinderGeometry(innerR * 0.99, innerR * 0.99, 0.05, 96), mat);
        topCap.position.y = (bodyHeight*0.5) + 0.025;
        S3.root.add(topCap);
        var bottomCap = new THREE.Mesh(new THREE.CylinderGeometry(innerR * 0.92, innerR * 0.92, 0.08, 96), mat);
        bottomCap.position.y = -(bodyHeight*0.5) - 0.04;
        S3.root.add(bottomCap);
        if (isBottle) return;
      }
      function placeGLB(glbRoot, p, outerR, bodyHeight){
        var THREE = window.THREE;
        glbRoot.updateMatrixWorld(true);
        var boxA = new THREE.Box3().setFromObject(glbRoot);
        var sizeA = boxA.getSize(new THREE.Vector3());
        var srcMax = Math.max(0.0001, Number(sizeA.x||0), Number(sizeA.y||0), Number(sizeA.z||0));
        var targetMax = Math.max(bodyHeight * 1.28, outerR * 2.45);
        var fitScale = (targetMax / srcMax) * p.scale;
        glbRoot.scale.setScalar(fitScale);
        glbRoot.rotation.y = p.rotY;
        glbRoot.updateMatrixWorld(true);
        var boxB = new THREE.Box3().setFromObject(glbRoot);
        var centerB = boxB.getCenter(new THREE.Vector3());
        glbRoot.position.sub(centerB);
        glbRoot.updateMatrixWorld(true);
        var boxC = new THREE.Box3().setFromObject(glbRoot);
        var centerC = boxC.getCenter(new THREE.Vector3());
        glbRoot.position.y -= centerC.y;
        glbRoot.position.x += p.offX;
        glbRoot.position.z += p.offZ;
        S3.root.add(glbRoot);
      }
      function rebuild(){
        var is3d = String($('#ds_preview_mode').val() || '2d') === '3d';
        if(!is3d) return;
        if(!S3.ready && !initScene()) return;
        clearRoot();
        var p = readParams();
        var l = buildLabel(p);
        if(!p.modelUrl || !window.THREE || !window.THREE.GLTFLoader){
          buildProceduralCup(l.outerR, l.bodyHeight, p.wallMm, p.type3d === 'bottle');
          setMeta('3D preview: procedural cup (no GLB loaded)');
          return;
        }
        var token = ++S3.loadToken;
        var loader = new window.THREE.GLTFLoader();
        loader.load(p.modelUrl, function(gltf){
          if(token !== S3.loadToken) return;
          try {
            var r = (gltf && gltf.scene) ? gltf.scene : null;
            if(!r){ buildProceduralCup(l.outerR, l.bodyHeight, p.wallMm, p.type3d === 'bottle'); setMeta('GLB loaded empty, fallback used'); return; }
            placeGLB(r, p, l.outerR, l.bodyHeight);
            setMeta('3D preview: GLB loaded. Drag = X/Y rotate, Shift+Drag = Z roll');
          } catch(e){
            buildProceduralCup(l.outerR, l.bodyHeight, p.wallMm, p.type3d === 'bottle');
            setMeta('GLB parse failed, fallback used');
          }
        }, undefined, function(){
          if(token !== S3.loadToken) return;
          buildProceduralCup(l.outerR, l.bodyHeight, p.wallMm, p.type3d === 'bottle');
          setMeta('GLB load failed, fallback used');
        });
      }
      ensureDeps(function(ok){
        if(!ok){ setMeta('Three.js/GLTFLoader unavailable'); return; }
        initScene();
        rebuild();
      });
      $(window).on('resize.ds3dlive', resize);
      $('#ds_preview_mode,#ds_preview_3d_type,#ds_model_url,#ds_model_scale,#ds_model_rotation_y,#ds_model_offset_x,#ds_model_offset_z,#ds_model_radius_mm,#ds_model_height_mm,#ds_model_wall_thickness_mm,#ds_blank_handle_mm,#ds_canvas_start_offset_mm,#ds_zone_canvas_width_mm,#ds_zone_canvas_height_mm,#ds_preview_config_json').on('input change', function(){
        rebuild();
      });
      $('#post').on('submit', function(){ if(S3.raf) window.cancelAnimationFrame(S3.raf); });
    }

    function syncAdvancedJsonUI(){
      var is3d = String($('#ds_preview_mode').val() || '2d') === '3d';
      var $btn = $('#ds_adv_json_toggle');
      var open = !!S.adv;
      if($btn.length){
        var tShow = String($btn.attr('data-show') || 'Show Advanced JSON');
        var tHide = String($btn.attr('data-hide') || 'Hide Advanced JSON');
        $btn.attr('aria-expanded', open ? 'true' : 'false');
        $btn.text(open ? tHide : tShow);
      }
      $('#ds_zones_json_row').toggle(open && !is3d);
      $('#ds_preview_config_json_row').toggle(open);
    }

    function syncPreviewModeUI(){
      var is3d = String($('#ds_preview_mode').val() || '2d') === '3d';
      $('#ds_mockup_builder_row').toggle(!is3d);
      $('#ds_3d_builder_row').toggle(is3d);
      $('#ds_zones_3d_row').toggle(is3d);
      // Prevent hidden mode fields from participating in native browser validation.
      // This avoids "invalid form control ... is not focusable" on publish.
      $('#ds_3d_builder_row').find('input,select,textarea,button').prop('disabled', !is3d);
      $('#ds_zones_3d_row').find('input,select,textarea,button').prop('disabled', !is3d);
      $('#ds_mockup_builder_row').find('input,select,textarea,button').prop('disabled', is3d);
      $('#ds_zones_json_row').find('input,select,textarea,button').prop('disabled', is3d);
      syncAdvancedJsonUI();
      if(is3d) sync3dZonesJsonFromFields();
    }
    $('#ds_preview_mode').on('change', syncPreviewModeUI);
    $('#ds_zone_canvas_id,#ds_zone_canvas_label,#ds_zone_canvas_width_mm,#ds_zone_canvas_height_mm').on('input change', sync3dZonesJsonFromFields);
    $('#ds_adv_json_toggle').on('click', function(e){
      e.preventDefault();
      S.adv = !S.adv;
      syncAdvancedJsonUI();
    });

    $('#post').on('submit', function(ev){
      H(); // always sync hidden JSON first
      var is3d = String($('#ds_preview_mode').val() || '2d') === '3d';
      if (is3d) {
        sync3dZonesJsonFromFields();
        return true;
      }
      E();
      var errs = [];
      var seenAreaIds = {};
      for (var mi = 0; mi < S.m.length; mi++) {
        var m = S.m[mi] || {};
        var zonesInMockup = {};
        var mName = String(m.name || ('Mockup ' + (mi + 1)));
        var areas = Array.isArray(m.areas) ? m.areas : [];
        if (!areas.length) {
          errs.push(mName + ': at least one area is required.');
          continue;
        }
        for (var ai = 0; ai < areas.length; ai++) {
          var a = areas[ai] || {};
          var label = String(a.label || ('Area ' + (ai + 1)));
          var areaId = sanitizeKey(a.id || '');
          var zoneId = sanitizeKey(a.zone_id || a.id || '');
          if (!areaId) {
            errs.push(mName + ' / ' + label + ': missing area ID.');
          } else if (seenAreaIds[areaId]) {
            errs.push(mName + ' / ' + label + ': duplicate area ID "' + areaId + '".');
          } else {
            seenAreaIds[areaId] = 1;
          }
          if (!zoneId) {
            errs.push(mName + ' / ' + label + ': missing zone_id.');
          } else if (zonesInMockup[zoneId]) {
            errs.push(mName + ' / ' + label + ': duplicate zone_id "' + zoneId + '" inside the same mockup.');
          } else {
            zonesInMockup[zoneId] = 1;
          }
        }
      }
      if (errs.length) {
        var originalStatus = String($('#original_post_status').val() || '').toLowerCase();
        // On first publish from auto-draft, don't hard-block save because admin form
        // can contain transient/incomplete metabox values while new post is created.
        if (originalStatus === 'auto-draft') {
          window.alert('Template mapping has warnings, but initial publish will continue:\n\n- ' + errs.slice(0, 8).join('\n- ') + (errs.length > 8 ? '\n- ...' : ''));
          return true;
        }
        ev.preventDefault();
        window.alert('Cannot save template until zone mapping is valid:\n\n- ' + errs.slice(0, 8).join('\n- ') + (errs.length > 8 ? '\n- ...' : ''));
        return false;
      }
      return true;
    });

    R();
    bind3dCalibrator();
    bind3dLivePreview();
    syncPreviewModeUI();
  });
})(jQuery);
JS;
        $js = str_replace(
            ['__DS_ADMIN_THREE_URL__', '__DS_ADMIN_GLTF_URL__'],
            [esc_js($three_vendor_url), esc_js($gltf_vendor_url)],
            $js
        );
        wp_register_script('ds-admin-inline', false, ['jquery'], '1.3.0', true);
        wp_enqueue_script('ds-admin-inline');
        wp_add_inline_script('ds-admin-inline', $js);
    }

    public static function add_meta_boxes() {
        add_meta_box('ds_template_dimensions', __('Template Dimensions', 'ds'), [__CLASS__, 'render_dimensions_meta_box'], 'ds_template', 'normal', 'high');
    }

    public static function render_dimensions_meta_box($post) {
        wp_nonce_field('ds_save_template_meta', 'ds_template_meta_nonce');
        $width_mm = (float) get_post_meta($post->ID, 'width_mm', true);
        $height_mm = (float) get_post_meta($post->ID, 'height_mm', true);
        $bleed_mm = (float) get_post_meta($post->ID, 'bleed_mm', true);
        $dpi = (int) get_post_meta($post->ID, 'dpi', true);
        $preview_type = (string) get_post_meta($post->ID, 'preview_type', true);
        $preview_mode = (string) get_post_meta($post->ID, 'preview_mode', true);
        $zones_json = (string) get_post_meta($post->ID, 'zones_json', true);
        $preview_raw = (string) get_post_meta($post->ID, 'preview_config_json', true);
        if ($preview_type === '') $preview_type = 'mug';
        if ($preview_mode === '') {
            $preview_mode = in_array($preview_type, ['mug', 'bottle'], true) ? '3d' : '2d';
        }
        if (!in_array($preview_mode, ['2d', '3d'], true)) {
            $preview_mode = '2d';
        }
        if ($zones_json === '') $zones_json = self::d_zones($preview_type);
        list($preview_json, $preview_obj) = self::parse_preview($preview_raw, $preview_type);
        $dw = $width_mm ?: 200;
        $dh = $height_mm ?: 90;
        $mockups = self::norm_mockups($preview_obj['mockups'] ?? [], $dw, $dh);
        $link_mockup_scale = !empty($preview_obj['link_mockup_scale']);
        $preview_3d_type = (string) ($preview_obj['preview_3d_type'] ?? (in_array($preview_type, ['mug', 'bottle', 'tshirt'], true) ? $preview_type : 'mug'));
        if (!in_array($preview_3d_type, ['mug', 'bottle', 'tshirt'], true)) $preview_3d_type = 'mug';
        $model_url = (string) ($preview_obj['model_url'] ?? '');
        $model_scale = (float) ($preview_obj['model_scale'] ?? 1.0);
        if (!$model_scale || $model_scale <= 0) $model_scale = 1.0;
        $model_rotation_y = (float) ($preview_obj['model_rotation_y'] ?? 0);
        $model_offset_x = (float) ($preview_obj['model_offset_x'] ?? 0.0);
        $model_offset_z = (float) ($preview_obj['model_offset_z'] ?? 0.0);
        $default_circ = ($preview_3d_type === 'bottle') ? 250.0 : 230.0;
        $default_radius = $default_circ / (3.141592653589793 * 2.0);
        $default_model_height = ($preview_3d_type === 'bottle') ? 180.0 : max(1.0, ($height_mm ?: 95.0));
        $model_radius_mm = (float) ($preview_obj['model_radius_mm'] ?? $default_radius);
        if ($model_radius_mm <= 0) $model_radius_mm = $default_radius;
        $model_height_mm = (float) ($preview_obj['model_height_mm'] ?? $default_model_height);
        if ($model_height_mm <= 0) $model_height_mm = $default_model_height;
        $default_wall_thickness = ($preview_3d_type === 'bottle') ? 2.6 : 3.2;
        $model_wall_thickness_mm = (float) ($preview_obj['model_wall_thickness_mm'] ?? $default_wall_thickness);
        if ($model_wall_thickness_mm <= 0) $model_wall_thickness_mm = $default_wall_thickness;
        $default_blank_mm = ($preview_3d_type === 'bottle') ? 38.0 : 45.0;
        $blank_handle_mm = (float) ($preview_obj['blank_handle_mm'] ?? $default_blank_mm);
        if ($blank_handle_mm < 0) $blank_handle_mm = $default_blank_mm;
        $canvas_start_offset_mm = (float) ($preview_obj['canvas_start_offset_mm'] ?? ($blank_handle_mm / 2.0));
        if ($canvas_start_offset_mm < 0) $canvas_start_offset_mm = ($blank_handle_mm / 2.0);
        $zone_canvas_id = 'wrap';
        $zone_canvas_label = 'Main';
        $zone_canvas_width_mm = $width_mm ?: 200;
        $zone_canvas_height_mm = $height_mm ?: 90;
        $zones_decoded = json_decode((string) $zones_json, true);
        if (is_array($zones_decoded) && !empty($zones_decoded[0]) && is_array($zones_decoded[0])) {
            $z0 = $zones_decoded[0];
            $zone_canvas_id = sanitize_key((string) ($z0['id'] ?? ($z0['zone_id'] ?? 'wrap')));
            if ($zone_canvas_id === '') $zone_canvas_id = 'wrap';
            $zone_canvas_label = sanitize_text_field((string) ($z0['label'] ?? 'Main'));
            if ($zone_canvas_label === '') $zone_canvas_label = 'Main';
            $zone_canvas_width_mm = max(1.0, (float) ($z0['width_mm'] ?? $zone_canvas_width_mm));
            $zone_canvas_height_mm = max(1.0, (float) ($z0['height_mm'] ?? $zone_canvas_height_mm));
        }
        $target_meshes_raw = '';
        if (!empty($preview_obj['target_meshes']) && is_array($preview_obj['target_meshes'])) {
            $target_meshes_raw = implode("\n", array_map('strval', $preview_obj['target_meshes']));
        }
        ?>
        <table class="form-table">
            <tr>
                <th><label for="ds_preview_mode"><?php _e('Preview Type', 'ds'); ?></label></th>
                <td>
                    <select id="ds_preview_mode" name="ds_preview_mode" class="regular-text">
                        <option value="2d" <?php selected($preview_mode, '2d'); ?>><?php _e('2D', 'ds'); ?></option>
                        <option value="3d" <?php selected($preview_mode, '3d'); ?>><?php _e('3D', 'ds'); ?></option>
                    </select>
                    <p class="description"><?php _e('Use 3D for mug. Use 2D for all flat products and mockups.', 'ds'); ?></p>
                </td>
            </tr>
            <tr style="display:none;">
                <th><label for="ds_width_mm"><?php _e('Width (mm)', 'ds'); ?></label></th>
                <td><input type="number" step="0.1" min="1" id="ds_width_mm" name="ds_width_mm" value="<?php echo esc_attr($width_mm ?: 200); ?>" class="regular-text" /></td>
            </tr>
            <tr style="display:none;">
                <th><label for="ds_height_mm"><?php _e('Height (mm)', 'ds'); ?></label></th>
                <td><input type="number" step="0.1" min="1" id="ds_height_mm" name="ds_height_mm" value="<?php echo esc_attr($height_mm ?: 90); ?>" class="regular-text" /></td>
            </tr>
            <tr style="display:none;">
                <th><label for="ds_bleed_mm"><?php _e('Bleed (mm)', 'ds'); ?></label></th>
                <td><input type="number" step="0.1" min="0" id="ds_bleed_mm" name="ds_bleed_mm" value="<?php echo esc_attr($bleed_mm ?: 3); ?>" class="regular-text" /></td>
            </tr>
            <tr style="display:none;">
                <th><label for="ds_dpi"><?php _e('DPI', 'ds'); ?></label></th>
                <td><input type="number" step="1" min="72" id="ds_dpi" name="ds_dpi" value="<?php echo esc_attr($dpi ?: 300); ?>" class="regular-text" /></td>
            </tr>
            <tr>
                <th><?php _e('Template Thumbnail', 'ds'); ?></th>
                <td>
                    <p class="description">
                        <?php _e('Use WordPress Featured Image for this template thumbnail. It is used in Step 1 template picker cards.', 'ds'); ?>
                    </p>
                    <?php
                    $thumb_id = get_post_thumbnail_id($post->ID);
                    $thumb_src = $thumb_id ? wp_get_attachment_image_url($thumb_id, 'medium') : '';
                    ?>
                    <?php if ($thumb_src): ?>
                        <p style="margin:8px 0;">
                            <img src="<?php echo esc_url($thumb_src); ?>" alt="" style="max-width:240px;height:auto;border:1px solid #d0d3d6;border-radius:8px;" />
                        </p>
                    <?php endif; ?>
                    <p style="margin:6px 0 0;">
                        <a class="button" href="<?php echo esc_url(get_edit_post_link($post->ID, '')); ?>#postimagediv">
                            <?php esc_html_e('Set / Change Featured Image', 'ds'); ?>
                        </a>
                    </p>
                </td>
            </tr>
            <tr id="ds_3d_builder_row" style="display:none;">
                <th><?php _e('3D Model Builder', 'ds'); ?></th>
                <td>
                    <p class="description"><?php _e('Choose built-in 3D type or set a custom GLB URL. Mug/Bottle keep existing behavior.', 'ds'); ?></p>
                    <div class="ds-grid">
                        <label><?php _e('3D Type', 'ds'); ?>
                            <select id="ds_preview_3d_type" name="ds_preview_3d_type">
                                <option value="mug" <?php selected($preview_3d_type, 'mug'); ?>><?php _e('Mug', 'ds'); ?></option>
                                <option value="bottle" <?php selected($preview_3d_type, 'bottle'); ?>><?php _e('Bottle', 'ds'); ?></option>
                                <option value="tshirt" <?php selected($preview_3d_type, 'tshirt'); ?>><?php _e('Custom GLB (T-shirt engine)', 'ds'); ?></option>
                            </select>
                        </label>
                        <label><?php _e('Model URL (GLB)', 'ds'); ?><input type="url" id="ds_model_url" name="ds_model_url" value="<?php echo esc_attr($model_url); ?>" placeholder="https://.../model.glb" /></label>
                        <label><?php _e('Model Scale', 'ds'); ?><input type="number" step="0.01" min="0.01" id="ds_model_scale" name="ds_model_scale" value="<?php echo esc_attr($model_scale); ?>" /></label>
                        <label><?php _e('Model Rotate Y (rad)', 'ds'); ?><input type="number" step="0.01" id="ds_model_rotation_y" name="ds_model_rotation_y" value="<?php echo esc_attr($model_rotation_y); ?>" /></label>
                        <label><?php _e('Model Offset X', 'ds'); ?><input type="number" step="0.01" id="ds_model_offset_x" name="ds_model_offset_x" value="<?php echo esc_attr($model_offset_x); ?>" /></label>
                        <label><?php _e('Model Offset Z', 'ds'); ?><input type="number" step="0.01" id="ds_model_offset_z" name="ds_model_offset_z" value="<?php echo esc_attr($model_offset_z); ?>" /></label>
                        <label><?php _e('Model Radius (mm)', 'ds'); ?><input type="number" step="any" min="1" id="ds_model_radius_mm" name="ds_model_radius_mm" value="<?php echo esc_attr($model_radius_mm); ?>" /></label>
                        <label><?php _e('Model Height (mm)', 'ds'); ?><input type="number" step="any" min="1" id="ds_model_height_mm" name="ds_model_height_mm" value="<?php echo esc_attr($model_height_mm); ?>" /></label>
                        <label><?php _e('Wall Thickness (mm)', 'ds'); ?><input type="number" step="any" min="0.5" id="ds_model_wall_thickness_mm" name="ds_model_wall_thickness_mm" value="<?php echo esc_attr($model_wall_thickness_mm); ?>" /></label>
                        <label><?php _e('No-Print Width (mm)', 'ds'); ?><input type="number" step="any" min="0" id="ds_blank_handle_mm" name="ds_blank_handle_mm" value="<?php echo esc_attr($blank_handle_mm); ?>" /></label>
                        <label><?php _e('No-Print Start Offset (mm)', 'ds'); ?><input type="number" step="any" min="0" id="ds_canvas_start_offset_mm" name="ds_canvas_start_offset_mm" value="<?php echo esc_attr($canvas_start_offset_mm); ?>" /></label>
                    </div>
                    <p style="margin-top:8px;">
                        <label for="ds_model_target_meshes"><strong><?php _e('Target Meshes (one per line)', 'ds'); ?></strong></label><br/>
                        <textarea id="ds_model_target_meshes" name="ds_model_target_meshes" rows="4" class="large-text code" placeholder="MeshName_1&#10;MeshName_2"><?php echo esc_textarea($target_meshes_raw); ?></textarea>
                    </p>
                    <div class="ds-3d-calib">
                        <strong><?php _e('Visual Calibration', 'ds'); ?></strong>
                        <p class="description" style="margin:4px 0 10px;"><?php _e('Top view calibration: cup is fixed in center. Drag only the print arc for Offset X/Z. Use sliders for scale and rotation.', 'ds'); ?></p>
                        <div class="ds-3d-calib-grid">
                            <div>
                                <div id="ds_3d_pad" class="ds-3d-pad">
                                    <span class="ds-3d-cup-fixed"></span>
                                    <span id="ds_3d_print_arc" class="ds-3d-print-arc"></span>
                                </div>
                                <div id="ds_3d_pad_readout" class="ds-3d-readout"></div>
                            </div>
                            <div class="ds-3d-sliders">
                                <label>
                                    <?php _e('Scale', 'ds'); ?>
                                    <input type="range" id="ds_model_scale_rng" min="0.2" max="2.5" step="0.01" value="<?php echo esc_attr($model_scale); ?>" />
                                </label>
                                <label>
                                    <?php _e('Rotate Y', 'ds'); ?>
                                    <input type="range" id="ds_model_rot_rng" min="-3.14" max="3.14" step="0.01" value="<?php echo esc_attr($model_rotation_y); ?>" />
                                </label>
                                <label>
                                    <?php _e('Canvas Scale', 'ds'); ?>
                                    <input type="range" id="ds_canvas_scale_rng" min="0.50" max="1.80" step="0.01" value="1.00" />
                                </label>
                            </div>
                        </div>
                    </div>
                    <div class="ds-3d-live">
                        <strong><?php _e('Live 3D Preview', 'ds'); ?></strong>
                        <p class="description" style="margin:4px 0 8px;"><?php _e('Real model preview with current calibration values (admin only).', 'ds'); ?></p>
                        <div id="ds_3d_live_stage" class="ds-3d-live-stage"></div>
                        <div id="ds_3d_live_meta" class="ds-3d-live-meta"></div>
                    </div>
                    <div id="ds_zones_3d_row" class="ds-zones3d-row" style="display:none;">
                        <h4><?php _e('3D Canvas', 'ds'); ?></h4>
                        <p class="description"><?php _e('Create printable canvas without manual JSON editing.', 'ds'); ?></p>
                        <div class="ds-zones3d-grid">
                            <label><?php _e('Canvas ID', 'ds'); ?><input type="text" id="ds_zone_canvas_id" name="ds_zone_canvas_id" value="<?php echo esc_attr($zone_canvas_id); ?>" /></label>
                            <label><?php _e('Canvas Label', 'ds'); ?><input type="text" id="ds_zone_canvas_label" name="ds_zone_canvas_label" value="<?php echo esc_attr($zone_canvas_label); ?>" /></label>
                            <label><?php _e('Width mm', 'ds'); ?><input type="number" step="any" min="1" id="ds_zone_canvas_width_mm" name="ds_zone_canvas_width_mm" value="<?php echo esc_attr($zone_canvas_width_mm); ?>" /></label>
                            <label><?php _e('Height mm', 'ds'); ?><input type="number" step="any" min="1" id="ds_zone_canvas_height_mm" name="ds_zone_canvas_height_mm" value="<?php echo esc_attr($zone_canvas_height_mm); ?>" /></label>
                        </div>
                    </div>
                </td>
            </tr>
            <tr id="ds_mockup_builder_row">
                <th><?php _e('Mockup Builder', 'ds'); ?></th>
                <td>
                    <p class="description"><?php _e('One mockup can have multiple printable areas. You edit values only for selected area. Add more mockups when needed.', 'ds'); ?></p>
                    <div class="ds-mb">
                        <div class="ds-box">
                            <div class="ds-list" data-r="ml"></div>
                            <button type="button" class="button" data-a="addm"><?php _e('Add Mockup', 'ds'); ?></button>
                            <button type="button" class="button" data-a="delm"><?php _e('Remove Mockup', 'ds'); ?></button>
                        </div>
                        <div class="ds-box">
                            <div class="ds-row">
                                <input type="text" class="regular-text" data-i="mn" placeholder="Mockup name" />
                                <select data-i="ms">
                                    <option value="custom"><?php _e('Side: Custom', 'ds'); ?></option>
                                    <option value="front"><?php _e('Side: Front', 'ds'); ?></option>
                                    <option value="back"><?php _e('Side: Back', 'ds'); ?></option>
                                    <option value="left"><?php _e('Side: Left', 'ds'); ?></option>
                                    <option value="right"><?php _e('Side: Right', 'ds'); ?></option>
                                </select>
                            </div>
                            <div class="ds-row" style="margin-top:6px;">
                                <label><input type="checkbox" data-i="link-scale" <?php checked($link_mockup_scale); ?> /> <?php _e('Use same scale across all mockups', 'ds'); ?></label>
                            </div>
                            <div class="ds-row" style="margin-top:8px;">
                                <input type="url" class="regular-text" data-i="mu" placeholder="https://..." />
                                <button type="button" class="button" data-a="up"><?php _e('Upload', 'ds'); ?></button>
                                <button type="button" class="button" data-a="clr"><?php _e('Clear', 'ds'); ?></button>
                            </div>
                            <div class="ds-prev" data-r="pv">
                                <img class="ds-img" data-r="img" src="" alt="" />
                                <div data-r="ov"></div>
                            </div>
                            <div class="ds-row" style="margin-top:8px;">
                                <button type="button" class="button" data-a="adda"><?php _e('Add Printable Area', 'ds'); ?></button>
                                <button type="button" class="button" data-a="dela"><?php _e('Remove Selected Area', 'ds'); ?></button>
                            </div>
                            <div class="ds-tag" data-r="al"></div>
                            <div class="ds-master-note" data-r="master-note"></div>
                            <div class="ds-grid">
                                <label><?php _e('Label', 'ds'); ?><input type="text" data-i="al" /></label>
                                <label><?php _e('Width mm', 'ds'); ?><input type="number" step="0.1" min="1" data-i="awm" /></label>
                                <label><?php _e('Height mm', 'ds'); ?><input type="number" step="0.1" min="1" data-i="ahm" /></label>
                                <label><?php _e('Exclude Areas', 'ds'); ?><select data-i="aexm" multiple size="4"></select></label>
                                <label><?php _e('X %', 'ds'); ?><input type="number" step="0.01" min="0" max="100" data-i="ax" /></label>
                                <label><?php _e('Y %', 'ds'); ?><input type="number" step="0.01" min="0" max="100" data-i="ay" /></label>
                                <label><?php _e('W %', 'ds'); ?><input type="number" step="0.01" min="1" max="100" data-i="aw" /></label>
                                <label><?php _e('H %', 'ds'); ?><input type="number" step="0.01" min="1" max="100" data-i="ah" /></label>
                                <label><input type="checkbox" data-i="aor" /> <?php _e('Allow Canvas Orientation (Portrait/Landscape)', 'ds'); ?></label>
                            </div>
                            <div class="ds-grid" data-r="subdims" style="display:none">
                                <label><?php _e('Portrait W mm', 'ds'); ?><input type="number" step="0.1" min="1" data-i="apw" /></label>
                                <label><?php _e('Portrait H mm', 'ds'); ?><input type="number" step="0.1" min="1" data-i="aph" /></label>
                                <label><?php _e('Landscape W mm', 'ds'); ?><input type="number" step="0.1" min="1" data-i="alw" /></label>
                                <label><?php _e('Landscape H mm', 'ds'); ?><input type="number" step="0.1" min="1" data-i="alh" /></label>
                            </div>
                            <p class="description"><?php _e('Select area names that cannot be combined with the selected area.', 'ds'); ?></p>
                        </div>
                    </div>
                    <textarea id="ds_mockups_json" name="ds_mockups_json" class="ds-hide"></textarea>
                    <input type="hidden" id="ds_link_mockup_scale" name="ds_link_mockup_scale" value="<?php echo $link_mockup_scale ? '1' : '0'; ?>" />
                    <script type="application/json" data-r="src"><?php echo wp_json_encode($mockups); ?></script>
                </td>
            </tr>
            <tr id="ds_advanced_json_toggle_row" class="ds-advanced-json-row">
                <th><?php _e('Advanced', 'ds'); ?></th>
                <td>
                    <button type="button" class="button" id="ds_adv_json_toggle" data-show="<?php echo esc_attr__('Show Advanced JSON', 'ds'); ?>" data-hide="<?php echo esc_attr__('Hide Advanced JSON', 'ds'); ?>" aria-expanded="false">
                        <?php _e('Show Advanced JSON', 'ds'); ?>
                    </button>
                    <p class="description ds-advanced-json-note"><?php _e('Use only for debugging/import-export. Standard setup is handled by builders above.', 'ds'); ?></p>
                </td>
            </tr>
            <tr id="ds_zones_json_row" style="display:none;"><th><label for="ds_zones_json"><?php _e('Zones (JSON)', 'ds'); ?></label></th><td><textarea id="ds_zones_json" name="ds_zones_json" rows="10" class="large-text code"><?php echo esc_textarea($zones_json); ?></textarea></td></tr>
            <tr id="ds_preview_config_json_row" style="display:none;"><th><label for="ds_preview_config_json"><?php _e('Preview Config (JSON)', 'ds'); ?></label></th><td><textarea id="ds_preview_config_json" name="ds_preview_config_json" rows="10" class="large-text code"><?php echo esc_textarea($preview_json); ?></textarea></td></tr>
        </table>
        <?php
    }

    public static function save_template_meta($post_id) {
        if (!isset($_POST['ds_template_meta_nonce']) || !wp_verify_nonce($_POST['ds_template_meta_nonce'], 'ds_save_template_meta')) return;
        if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return;
        if (!current_user_can('edit_post', $post_id)) return;
        if (wp_is_post_revision($post_id)) return;

        $width_mm = isset($_POST['ds_width_mm']) ? floatval($_POST['ds_width_mm']) : null;
        $height_mm = isset($_POST['ds_height_mm']) ? floatval($_POST['ds_height_mm']) : null;
        $bleed_mm = isset($_POST['ds_bleed_mm']) ? floatval($_POST['ds_bleed_mm']) : null;
        $dpi = isset($_POST['ds_dpi']) ? intval($_POST['ds_dpi']) : null;
        $preview_mode = isset($_POST['ds_preview_mode']) ? sanitize_text_field(wp_unslash($_POST['ds_preview_mode'])) : '2d';
        $preview_3d_type = isset($_POST['ds_preview_3d_type']) ? sanitize_text_field(wp_unslash($_POST['ds_preview_3d_type'])) : 'mug';
        $zones_json_raw = isset($_POST['ds_zones_json']) ? wp_unslash($_POST['ds_zones_json']) : '';
        $zone_canvas_id = isset($_POST['ds_zone_canvas_id']) ? sanitize_key(wp_unslash($_POST['ds_zone_canvas_id'])) : 'wrap';
        if ($zone_canvas_id === '') $zone_canvas_id = 'wrap';
        $zone_canvas_label = isset($_POST['ds_zone_canvas_label']) ? sanitize_text_field(wp_unslash($_POST['ds_zone_canvas_label'])) : 'Main';
        if ($zone_canvas_label === '') $zone_canvas_label = 'Main';
        $zone_canvas_width_mm = isset($_POST['ds_zone_canvas_width_mm']) ? (float) wp_unslash($_POST['ds_zone_canvas_width_mm']) : 0.0;
        $zone_canvas_height_mm = isset($_POST['ds_zone_canvas_height_mm']) ? (float) wp_unslash($_POST['ds_zone_canvas_height_mm']) : 0.0;
        $preview_json_raw = isset($_POST['ds_preview_config_json']) ? wp_unslash($_POST['ds_preview_config_json']) : '';
        $mockups_json_raw = isset($_POST['ds_mockups_json']) ? wp_unslash($_POST['ds_mockups_json']) : '[]';
        $link_mockup_scale = !empty($_POST['ds_link_mockup_scale']) && wp_unslash($_POST['ds_link_mockup_scale']) === '1';
        $model_url = isset($_POST['ds_model_url']) ? esc_url_raw(wp_unslash($_POST['ds_model_url'])) : '';
        $model_scale = isset($_POST['ds_model_scale']) ? floatval(wp_unslash($_POST['ds_model_scale'])) : 1.0;
        if ($model_scale <= 0) $model_scale = 1.0;
        $model_rotation_y = isset($_POST['ds_model_rotation_y']) ? floatval(wp_unslash($_POST['ds_model_rotation_y'])) : 0.0;
        $model_offset_x = isset($_POST['ds_model_offset_x']) ? floatval(wp_unslash($_POST['ds_model_offset_x'])) : 0.0;
        $model_offset_z = isset($_POST['ds_model_offset_z']) ? floatval(wp_unslash($_POST['ds_model_offset_z'])) : 0.0;
        $model_radius_mm = isset($_POST['ds_model_radius_mm']) ? floatval(wp_unslash($_POST['ds_model_radius_mm'])) : 0.0;
        $model_height_mm = isset($_POST['ds_model_height_mm']) ? floatval(wp_unslash($_POST['ds_model_height_mm'])) : 0.0;
        $model_wall_thickness_mm = isset($_POST['ds_model_wall_thickness_mm']) ? floatval(wp_unslash($_POST['ds_model_wall_thickness_mm'])) : 0.0;
        $blank_handle_mm = isset($_POST['ds_blank_handle_mm']) ? floatval(wp_unslash($_POST['ds_blank_handle_mm'])) : -1.0;
        $canvas_start_offset_mm = isset($_POST['ds_canvas_start_offset_mm']) ? floatval(wp_unslash($_POST['ds_canvas_start_offset_mm'])) : -1.0;
        if ($model_radius_mm <= 0) {
            $model_radius_mm = (($preview_3d_type === 'bottle') ? 250.0 : 230.0) / (3.141592653589793 * 2.0);
        }
        if ($model_height_mm <= 0) {
            $model_height_mm = ($preview_3d_type === 'bottle') ? 180.0 : max(1.0, (float) ($height_mm ?: 95.0));
        }
        if ($model_wall_thickness_mm <= 0) {
            $model_wall_thickness_mm = ($preview_3d_type === 'bottle') ? 2.6 : 3.2;
        }
        if ($blank_handle_mm < 0) {
            $blank_handle_mm = ($preview_3d_type === 'bottle') ? 38.0 : 45.0;
        }
        if ($canvas_start_offset_mm < 0) {
            $canvas_start_offset_mm = $blank_handle_mm / 2.0;
        }
        $target_meshes_raw = isset($_POST['ds_model_target_meshes']) ? (string) wp_unslash($_POST['ds_model_target_meshes']) : '';
        $target_meshes = preg_split('/[\r\n,]+/', $target_meshes_raw);
        $target_meshes = array_values(array_filter(array_map(function($v){
            return sanitize_text_field(trim((string) $v));
        }, is_array($target_meshes) ? $target_meshes : [])));

        if (!in_array($preview_mode, ['2d', '3d'], true)) {
            $preview_mode = '2d';
        }

        if (!in_array($preview_3d_type, ['mug', 'bottle', 'tshirt'], true)) {
            $preview_3d_type = 'mug';
        }

        $preview_type = ($preview_mode === '3d') ? $preview_3d_type : 'tshirt';

        $zones_json = trim((string) $zones_json_raw);
        if ($preview_mode === '3d') {
            $zw = max(1.0, $zone_canvas_width_mm > 0 ? $zone_canvas_width_mm : ($width_mm ?: 200));
            $zh = max(1.0, $zone_canvas_height_mm > 0 ? $zone_canvas_height_mm : ($height_mm ?: 90));
            // In 3D, canvas dimensions are authoritative; keep template dimensions aligned.
            $width_mm = $zw;
            $height_mm = $zh;
            $zones_json = wp_json_encode([[
                'id' => $zone_canvas_id,
                'zone_id' => $zone_canvas_id,
                'label' => $zone_canvas_label,
                'width_mm' => $zw,
                'height_mm' => $zh,
            ]], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
        if ($zones_json === '') $zones_json = self::d_zones($preview_type);
        $z = json_decode($zones_json, true);
        if (!is_array($z)) $zones_json = self::d_zones($preview_type);

        list($preview_json, $preview_obj) = self::parse_preview($preview_json_raw, $preview_type);
        $mockups = self::norm_mockups(json_decode((string) $mockups_json_raw, true), $width_mm ?: 200, $height_mm ?: 90);
        if ($preview_mode === '2d') {
            $zones = self::zones_from_mockups($mockups, $width_mm ?: 200, $height_mm ?: 90);
            if (!empty($zones[0]) && is_array($zones[0])) {
                $width_mm = max(1.0, (float) ($zones[0]['width_mm'] ?? ($width_mm ?: 200)));
                $height_mm = max(1.0, (float) ($zones[0]['height_mm'] ?? ($height_mm ?: 90)));
            }
            $zones_json = wp_json_encode($zones, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        }
        $preview_obj['mockups'] = $mockups;
        $preview_obj['link_mockup_scale'] = (bool) $link_mockup_scale;
        $preview_obj['preview_3d_type'] = $preview_3d_type;
        if ($model_url !== '') {
            $preview_obj['model_url'] = $model_url;
        } elseif (isset($preview_obj['model_url'])) {
            unset($preview_obj['model_url']);
        }
        $preview_obj['model_scale'] = $model_scale;
        $preview_obj['model_rotation_y'] = $model_rotation_y;
        $preview_obj['model_offset_x'] = $model_offset_x;
        $preview_obj['model_offset_z'] = $model_offset_z;
        $preview_obj['model_radius_mm'] = $model_radius_mm;
        $preview_obj['model_height_mm'] = $model_height_mm;
        $preview_obj['model_wall_thickness_mm'] = $model_wall_thickness_mm;
        $preview_obj['blank_handle_mm'] = max(0.0, (float) $blank_handle_mm);
        $preview_obj['canvas_start_offset_mm'] = max(0.0, (float) $canvas_start_offset_mm);
        $preview_obj['handle_theta_rad'] = -1.5707963267948966;
        $preview_obj['target_meshes'] = $target_meshes;

        $legacy = self::legacy_map($mockups);
        $preview_obj['mockup_2d'] = $legacy['mockup_2d'];
        $preview_obj['mockup_2d_areas'] = $legacy['mockup_2d_areas'];
        $preview_json = wp_json_encode($preview_obj, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

        if ($width_mm) update_post_meta($post_id, 'width_mm', $width_mm);
        if ($height_mm) update_post_meta($post_id, 'height_mm', $height_mm);
        if ($bleed_mm !== null) update_post_meta($post_id, 'bleed_mm', $bleed_mm);
        if ($dpi) update_post_meta($post_id, 'dpi', $dpi);
        update_post_meta($post_id, 'preview_mode', $preview_mode);
        update_post_meta($post_id, 'preview_type', $preview_type);
        update_post_meta($post_id, 'zones_json', $zones_json);
        update_post_meta($post_id, 'preview_config_json', $preview_json);
        update_post_meta($post_id, 'mockup_front_url', (string) ($legacy['mockup_2d']['front'] ?? ''));
        update_post_meta($post_id, 'mockup_back_url', (string) ($legacy['mockup_2d']['back'] ?? ''));
        update_post_meta($post_id, 'mockup_left_url', (string) ($legacy['mockup_2d']['left'] ?? ''));
        update_post_meta($post_id, 'mockup_right_url', (string) ($legacy['mockup_2d']['right'] ?? ''));

        // Keep template usable in frontend/Woo flows that only consume published templates.
        $post = get_post($post_id);
        if (!$post || $post->post_type !== 'ds_template') return;

        $needs_update = false;
        $post_update = ['ID' => (int) $post_id];
        $raw_title = trim((string) $post->post_title);
        if ($raw_title === '') {
            $post_update['post_title'] = 'Template #' . (int) $post_id;
            $needs_update = true;
        }

        $status = (string) $post->post_status;
        $can_publish_template = current_user_can('publish_post', $post_id) || current_user_can('publish_posts');
        if (in_array($status, ['auto-draft', 'draft', 'pending'], true) && $can_publish_template) {
            $post_update['post_status'] = 'publish';
            $needs_update = true;
        }

        if ($needs_update) {
            remove_action('save_post_ds_template', [__CLASS__, 'save_template_meta']);
            wp_update_post($post_update);
            add_action('save_post_ds_template', [__CLASS__, 'save_template_meta']);
        }
    }
}

