// ─────────────────────────────────────────────
// DO_DetailView.jsx  (V7)
//
// V7 layout engine — renders nested JSON directly:
//   detail.layout → pages (tabs)
//   page.rows     → rows
//   row.cols      → cols (width 1–12)
//   col_id refs   → objects from objects[]
//
// V6 diff:
//   - No buildLayout() — JSON IS the layout
//   - No flat allObjects — receives layoutData prop
//   - Fields: field_code (fN) not field attribute
//   - Tabs: pages in detail.layout[], not tabDefs[]
//   - Objects: col_id reference not row/col numbers
//   - Theme overrides per-layout (nav_bg, font, etc.)
//
// Depends on (load before in index.html):
//   DO_Shared.jsx, DO_Fields_*.jsx, DO_UploadFields.jsx
// ─────────────────────────────────────────────

// ─────────────────────────────────────────────
// RenderObject — dispatches to field components
// V7: obj.field_code is the fN key (not obj.field)
// ─────────────────────────────────────────────
function RenderObject({ obj, record, onChange, onBlur, onForceSave,
                        valueListMap, appId, tCode, recordId, appData, onSaved, apiKeys }) {
  // V7 uses field_code (f1, f2…) instead of V6 field
  const fieldObj = { ...obj, field: obj.field_code || obj.field };

  const props = { obj: fieldObj, value: record?.[fieldObj.field] || '',
                  onChange, onBlur, valueListMap };

  const bucketMap = {
    east:'https://east.dataobjects.com', west:'https://west.dataobjects.com',
    central:'https://central.dataobjects.com', eu:'https://eu.dataobjects.com',
    uk:'https://uk.dataobjects.com', jp:'https://jp.dataobjects.com',
    au:'https://au.dataobjects.com',
  };
  const bucket      = bucketMap[appData] || bucketMap['east'];
  const uploadProps = { obj: fieldObj, value: record?.[fieldObj.field] || '',
                        appId, table: tCode, recordId, bucket, onSaved };

  switch (obj.type) {
    case 'input':
    case 'textarea':  return <TextareaField  {...props} />;
    case 'number': {
      const numObj = {
        ...fieldObj,
        format:         obj.format         || '',
        currencySymbol: obj.currencySymbol || '$',
        decimals:       obj.decimals       ?? (obj.format === 'currency' ? 2 : 0),
        min:            obj.min            ?? '',
        max:            obj.max            ?? '',
        align:          obj.align          || 'right',
      };
      return <NumberField obj={numObj} value={record?.[fieldObj.field] || ''}
               onChange={onChange} onBlur={onBlur} />;
    }
    case 'date': {
      const dateObj = {
        ...fieldObj,
        dateformat: obj.dateformat || 'MM/DD/YYYY',
        align:      obj.align      || 'left',
      };
      return <DateField obj={dateObj} value={record?.[fieldObj.field] || ''}
               onChange={onChange} onBlur={onBlur} />;
    }
    case 'time': {
      const timeObj = {
        ...fieldObj,
        timeformat: obj.timeformat || '12',
        align:      obj.align      || 'left',
      };
      return <TimeField obj={timeObj} value={record?.[fieldObj.field] || ''}
               onChange={onChange} onBlur={onBlur} />;
    }
    case 'datetime':  return <DateTimeField  {...props} />;
    case 'select':
    case 'checkbox':
    case 'radio': {
      // Translate V7 JSON → V6 SelectField expectations
      // V7: multi:true/false  →  V6: behavior:'multi'/'single'
      // V7: inline_options:"foo|F\nbar|B"  →  V6: source:'inline', options:[...]
      // V7: valuelist_name:"Cars"  →  V6: source:'local', valuelist:"Cars"

      // Parse inline options from V7 text format
      let inlineOptions = [];
      if (obj.options_source === 'inline' && obj.inline_options) {
        inlineOptions = obj.inline_options
          .split('\n').map(s => s.trim()).filter(Boolean)
          .map(line => {
            const p = line.indexOf(' | ');
            return p !== -1
              ? { label: line.slice(0, p).trim(), value: line.slice(p + 3).trim() }
              : line;
          });
      }

      const selectObj = {
        ...fieldObj,
        // V6 behavior field
        behavior: obj.multi === true ? 'multi' : 'single',
        // V6 source + options
        source:   obj.options_source === 'inline' ? 'inline' : 'local',
        options:  obj.options_source === 'inline' ? inlineOptions : undefined,
        // V6 valuelist name for local source
        valuelist: obj.valuelist_name || obj.valuelist || '',
        // V6 display/layout for checkbox orientation
        display:  obj.display || 'horizontal',
      };

      return <SelectField obj={selectObj} value={record?.[fieldObj.field] || ''}
               onChange={onChange} onBlur={onBlur} valueListMap={valueListMap} />;
    }
    case 'text':      return <TextField obj={fieldObj} record={record} />;
    case 'rating': {
      const ratingObj = {
        ...fieldObj,
        ratingEmoji: obj.emoji    || '⭐',
        ratingScale: obj.maxstars || 5,
      };
      return <RatingField obj={ratingObj} record={record}
               onChange={onChange} onForceSave={onForceSave} />;
    }
    case 'divider': {
      const dColor  = obj.color && obj.color.trim() ? obj.color.trim() : null;
      const dHeight = parseInt(obj.height) || 8;
      return (
        <div style={{
          height:          `${dHeight}px`,
          backgroundColor: dColor || 'transparent',
          margin:          '4px 0',
        }} />
      );
    }
    case 'spacer':    return <div style={{ height:(obj.height||20)+'px' }} />;
    case 'file':
    case 'image':
    case 'gallery': {
      // V7 stores display as obj.display ('image'|'gallery'|'file')
      // V6 components read obj.display directly — same field name, compatible
      const fileObj = {
        ...fieldObj,
        display:      obj.display      || 'gallery',
        // Single image
        width:        obj.width        || '100%',
        maxHeight:    obj.maxHeight    || '300',
        align:        obj.align        || 'center',
        // Gallery
        thumbSize:    obj.thumbSize    || 100,
        displayRows:  obj.displayRows  || 'all',
        // Permissions
        allowCamera:  obj.allowCamera  !== false,
        allowUpload:  obj.allowUpload  !== false,
        allowDownload:obj.allowDownload !== false,
        allowDelete:  obj.allowDelete  !== false,
        // File list columns
        showSize:     obj.showSize  !== false,
        showDate:     obj.showDate  !== false,
        showUser:     obj.showUser  !== false,
      };
      const d = fileObj.display;
      if (d === 'image')   return <ImageField   {...uploadProps} obj={fileObj} />;
      if (d === 'gallery') return <GalleryField {...uploadProps} obj={fileObj} />;
      return <FileField {...uploadProps} obj={fileObj} />;
    }
    case 'camera':  return <CameraField   {...uploadProps} />;
    case 'portal': {
      const portalObj = {
        ...fieldObj,
        // V7 stores these directly — pass through to PortalField
        relatedTable:  obj.relatedTable  || obj.portal_table || '',
        displayFields: obj.displayFields || [],
        displayAs:     obj.displayAs     || 'rows',
        sortField:     obj.sortField     || '',
        sortDir:       obj.sortDir       || 'desc',
        allowAdd:      obj.allowAdd      !== 'no'  && obj.allowAdd    !== false,
        allowDelete:   obj.allowDelete   !== 'no'  && obj.allowDelete !== false,
        deleteParent:  obj.deleteParent  === 'yes' || obj.deleteParent === true,
        portalHeight:  obj.portalHeight  || 0,
        portalRows:    obj.portalRows    || 25,
      };
      return <PortalField obj={portalObj} appId={appId}
               table={tCode} recordId={recordId} appData={appData} />;
    }
    case 'map': {
      const mapObj = {
        ...fieldObj,
        // V6 MapField reads linkedField (full) and addressMode
        linkedField:  obj.field_code    || obj.field        || '',
        addressMode:  obj.mapmode       || 'full',
        // Partial mode fields — V6 reads fieldAddress, fieldCity etc.
        fieldAddress: obj.field_address || '',
        fieldCity:    obj.field_city    || '',
        fieldState:   obj.field_state   || '',
        fieldZip:     obj.field_zip     || '',
        fieldCountry: obj.field_country || '',
        // V6 reads mapType (camelCase), zoom, mapHeight
        mapType:      obj.maptype       || 'roadmap',
        zoom:         obj.zoom          ?? 15,
        mapHeight:    obj.mapheight     || 400,
        showAddress:  obj.showcaption   !== false,
      };
      return <MapField obj={mapObj} value={record?.[mapObj.linkedField] || ''}
               record={record} apiKeys={apiKeys} />;
    }
    default:        return <div style={{ fontSize:11, color:'#94a3b8',
                             padding:'4px', fontFamily:'DM Mono, monospace' }}>
                             ? {obj.type}
                           </div>;
  }
}

// ─────────────────────────────────────────────
// Tab bar — renders pages as tabs
// Supports underline / tabs / pills styles
// ─────────────────────────────────────────────
function TabBar({ pages, activeId, tabStyle, tabSticky, theme, onSelect }) {
  const isUnderline = !tabStyle || tabStyle === 'underline';
  const isPills     = tabStyle === 'pills';
  const isTabs      = tabStyle === 'tabs';
  const navBg       = theme?.nav_bg  || '#0f2744';
  const navColor    = theme?.nav_color || '#ffffff';

  const containerStyle = {
    display:      'flex',
    alignItems:   'center',
    gap:          isPills ? 6 : 0,
    padding:      isPills ? '8px 0 6px' : isTabs ? '8px 0 0' : '0',
    borderBottom: isUnderline ? '2px solid #e2e8f0' : 'none',
    marginBottom: isTabs ? 0 : 8,
    flexWrap:     'wrap',
    ...(tabSticky ? {
      position:   'sticky',
      top:        54,         // below nav bar
      background: '#fff',
      zIndex:     10,
      boxShadow:  '0 2px 4px rgba(0,0,0,0.05)',
    } : {}),
  };

  return (
    <div style={containerStyle}>
      {pages.map(page => {
        const active = String(page.id) === String(activeId);
        if (isPills) return (
          <button key={page.id} onClick={() => onSelect(page.id)}
            style={{ padding:'6px 16px', borderRadius:20, border:'none', fontSize:'1rem',
                     fontWeight:600, cursor:'pointer', fontFamily:'inherit',
                     background: active ? navBg : '#f1f5f9',
                     color:      active ? navColor : '#64748b',
                     transition:'all 0.15s' }}>
            {page.name}
          </button>
        );
        if (isTabs) return (
          <button key={page.id} onClick={() => onSelect(page.id)}
            style={{ padding:'9px 20px', border:'1px solid #e2e8f0',
                     borderBottom: active ? '1px solid #fff' : '1px solid #e2e8f0',
                     borderRadius:'6px 6px 0 0', fontSize:13, fontWeight:600,
                     cursor:'pointer', fontFamily:'inherit', marginBottom:-1,
                     background: active ? '#fff' : '#f8fafc',
                     color: active ? '#1e293b' : '#64748b' }}>
            {page.name}
          </button>
        );
        // underline (default)
        return (
          <button key={page.id} onClick={() => onSelect(page.id)}
            style={{ padding:'10px 18px', border:'none',
                     borderBottom: active ? `2px solid ${navBg}` : '2px solid transparent',
                     background:'none', fontSize:'1rem',
                     fontWeight: active ? 700 : 500, cursor:'pointer',
                     fontFamily:'inherit', marginBottom:'-2px',
                     color: active ? navBg : '#64748b',
                     transition:'color 0.15s' }}>
            {page.name}
          </button>
        );
      })}
    </div>
  );
}

// ─────────────────────────────────────────────
// ColRenderer — renders one col and its objects
// V7: objects filtered by col_id
// ─────────────────────────────────────────────

function useWindowWidth() {
  const [w, setW] = React.useState(window.innerWidth);
  React.useEffect(() => {
    const h = () => setW(window.innerWidth);
    window.addEventListener('resize', h);
    return () => window.removeEventListener('resize', h);
  }, []);
  return w;
}

function ColRenderer({ col, objects, record, onChange, onBlur, onForceSave,
                       valueListMap, appId, tCode, recordId, appData, onSaved, apiKeys }) {
  const colObjects = (objects || [])
    .filter(o => o.col_id === col.id && o.status !== 0)
    .sort((a, b) => (a.order||0) - (b.order||0));

  const winW = useWindowWidth();
  const pct  = winW <= 900 ? '100%' : `${(col.width || 12) / 12 * 100}%`;

  return (
    <div className="do-col" style={{ flex:`0 0 ${pct}`, width:pct, boxSizing:'border-box' }}>
      {colObjects.map(obj => (
        <RenderObject key={obj.id || obj._oid}
          obj={obj} record={record}
          onChange={onChange} onBlur={onBlur} onForceSave={onForceSave}
          valueListMap={valueListMap} appId={appId} tCode={tCode}
          recordId={recordId} appData={appData} onSaved={onSaved} apiKeys={apiKeys}
        />
      ))}
    </div>
  );
}

// ─────────────────────────────────────────────
// RowRenderer — renders one row and its cols
// ─────────────────────────────────────────────
function RowRenderer({ row, objects, record, onChange, onBlur, onForceSave,
                       valueListMap, appId, tCode, recordId, appData, onSaved, apiKeys }) {
  return (
    <div className="do-row">
      {(row.cols || []).map(col => (
        <ColRenderer key={col.id}
          col={col}
          objects={objects}
          record={record}
          onChange={onChange}
          onBlur={onBlur}
          onForceSave={onForceSave}
          valueListMap={valueListMap}
          appId={appId}
          tCode={tCode}
          recordId={recordId}
          appData={appData}
          onSaved={onSaved}
          apiKeys={apiKeys}
        />
      ))}
    </div>
  );
}

// ─────────────────────────────────────────────
// Skeleton loader
// ─────────────────────────────────────────────
function DetailSkeleton() {
  return (
    <div style={{ padding:'0 8px' }}>
      {[1,2].map(i => (
        <div key={i} style={{ display:'flex', gap:16, marginBottom:20 }}>
          {[30, 40, 30].map((w, j) => (
            <div key={j} style={{ width:`${w}%` }}>
              <div style={{ height:10, background:'#f1f5f9', borderRadius:4,
                            marginBottom:8, width:'60%' }} />
              <div style={{ height:36, background:'#f1f5f9', borderRadius:6 }} />
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}

// ─────────────────────────────────────────────
// DODetailView — main component
// ─────────────────────────────────────────────
const DODetailView = React.forwardRef(function DODetailView({
  tableRow,
  layoutData,
  appSettings,
  apiKeys = {},
  theme,
  startId,
  isNewRecord,
  searchOpen,
  onSearchClose,
  foundSet,
  foundTotal,
  onFoundSetChange,
  onBack,
  onTableSelect,
  onRecordChange,
  onSavingChange,
  onSavedChange,
  onDirtyChange,
  onLoadingChange,
  onNewChange,
}, ref) {

  const tCode     = tableRow?.tCode || tableRow?.code || '';
  const tNum      = tableRow?.tNum  || (tCode.replace('t',''));
  const saveMode  = appSettings?.save || 'manual';
  const appData   = window.APP_DATA || 'east';

  // Layout from prop (pre-fetched by App.jsx)
  const detail    = layoutData?.detail   || null;
  const objects   = layoutData?.objects  || [];

  // Build value list map — keyed by both name and id for V7/V6 compat
  const valueListMap = React.useMemo(() => {
    const map = {};
    // From dedicated valuelists array in layoutData (V7)
    (layoutData?.valuelists || []).forEach(vl => {
      const opts = (vl.options || []).map(o =>
        typeof o === 'string' ? { label: o, value: o } : o
      );
      if (vl.name) map[vl.name] = opts;
      if (vl.id)   map[vl.id]   = opts;
    });
    // Fallback: valuelist objects mixed into objects array (V6 compat)
    (objects || []).filter(o => o.group === 'valuelist').forEach(vl => {
      const opts = (vl.options || []).map(o =>
        typeof o === 'string' ? { label: o, value: o } : o
      );
      if (vl.name) map[vl.name] = opts;
      if (vl.id)   map[vl.id]   = opts;
    });
    return map;
  }, [layoutData?.valuelists, objects]);

  // State
  const [record,       setRecord]       = React.useState(isNewRecord ? {} : null);
  const [recordId,     setRecordId]     = React.useState(startId || 1);
  const [isNew,        setIsNew]        = React.useState(isNewRecord || false);
  const [loading,      setLoading]      = React.useState(true);
  const [saving,       setSaving]       = React.useState(false);
  const [saved,        setSaved]        = React.useState(false);
  const [dirty,        setDirty]        = React.useState(false);
  const [error,        setError]        = React.useState(null);
  const [activePage,   setActivePage]   = React.useState(null);

  // Search state
  const [searchGlobal, setSearchGlobal] = React.useState('');
  const [searchField,  setSearchField]  = React.useState('');
  const [searchOp,     setSearchOp]     = React.useState('contains');
  const [searchValue,  setSearchValue]  = React.useState('');
  const [searchMsg,    setSearchMsg]    = React.useState(null);
  const [searching,    setSearching]    = React.useState(false);

  const schemaFields = (layoutData?.schema?.fields || []).filter(f => f.status !== 0);

  async function handleSearch() {
    let params;
    if (searchGlobal.trim()) {
      params = new URLSearchParams({
        app: APP_ID, table: tCode,
        fields: 'r_auto', limit: 5000, offset: 0,
        search: searchGlobal.trim(),
        searchfields: schemaFields.map(f => f.code).join(','),
      });
    } else if (searchField && searchValue.trim()) {
      params = new URLSearchParams({
        app: APP_ID, table: tCode,
        fields: 'r_auto', limit: 5000, offset: 0,
        field: searchField, op: searchOp, value: searchValue.trim(),
      });
    } else return;

    setSearching(true);
    setSearchMsg(null);
    try {
      const res  = await apiFetch(`${API_BASE}/v6/records?${params}`);
      const data = await res.json();
      if (data.status !== 'ok') throw new Error(data.message);
      const ids = (data.records || []).map(r => r.r_auto);
      if (!ids.length) { setSearchMsg('No records found'); setSearching(false); return; }
      onFoundSetChange && onFoundSetChange(ids, data.total, 25, 0);
      setRecordId(ids[0]);
      setSearchMsg(`${ids.length} record${ids.length > 1 ? 's' : ''} found`);
      onSearchClose && onSearchClose();
    } catch(err) { setSearchMsg(err.message); }
    setSearching(false);
  }

  function clearSearch() {
    setSearchGlobal(''); setSearchField(''); setSearchValue('');
    setSearchMsg(null);
    // Restore full found set
    apiFetch(`${API_BASE}/v6/records?app=${APP_ID}&table=${tCode}&fields=r_auto&limit=5000&offset=0`)
      .then(r => r.json())
      .then(d => { if (d.status === 'ok') onFoundSetChange && onFoundSetChange(d.records.map(r => r.r_auto), d.total, 25, 0); })
      .catch(() => {});
  }

  // Active page defaults to first page
  React.useEffect(() => {
    if (detail?.layout?.length > 0 && !activePage) {
      setActivePage(detail.layout[0].id);
    }
  }, [detail]);

  // Refs
  // ── DO Dev panels ─────────────────────────────
  const DO_DEV_IP = '208.67.224.30';
  const isDoDev   = () => String(window.__DO_CLIENT_IP__ || '').includes('208.67.224.30');
  const [apiLog,  setApiLog]  = React.useState([]);
  const [logOpen, setLogOpen] = React.useState(true);
  const [jwtOpen, setJwtOpen] = React.useState(false);

  function devLog(msg, type='info') {
    if (!isDoDev()) return;
    const ts = new Date().toLocaleTimeString();
    setApiLog(prev => [...prev.slice(-49), { ts, msg, type }]);
  }

  const recordRef  = React.useRef(record);
  const dirtyRef   = React.useRef(dirty);
  const isNewRef   = React.useRef(isNew);
  const pendingRef = React.useRef({});
  const timeoutRef = React.useRef(null);
  recordRef.current = record;
  dirtyRef.current  = dirty;
  isNewRef.current  = isNew;

  // ── Fetch record ──────────────────────────────
  const fetchRecord = React.useCallback((id) => {
    setLoading(true);
    setDirty(false);
    setIsNew(false);
    setError(null);
    pendingRef.current = {};
    onNewChange    && onNewChange(false);
    onLoadingChange && onLoadingChange(true);
    devLog(`Fetching record id=${id}...`);

    apiFetch(`${API_BASE}/v6/record?app=${APP_ID}&table=${tCode}&id=${id}`)
      .then(r => r.json())
      .then(data => {
        if (data.status !== 'ok') throw new Error(data.message);
        setRecord(data.record);
        setRecordId(data.record.r_auto);
        devLog(`✅ Record r_auto=${data.record.r_auto}`, 'ok');
        onRecordChange  && onRecordChange(data.record);
        onLoadingChange && onLoadingChange(false);
      })
      .catch(err => {
        setError(`Record: ${err.message}`);
        onLoadingChange && onLoadingChange(false);
      })
      .finally(() => setLoading(false));
  }, [tCode]);

  // ── Save record ───────────────────────────────
  const saveRecord = React.useCallback(async (rec, fields) => {
    if (!rec) return;
    const keys = fields
      ? Object.keys(fields)
      : Object.keys(rec).filter(k => /^f\d+$/.test(k));
    if (!keys.length) return;

    setSaving(true);
    onSavingChange && onSavingChange(true);
    const body = {};
    keys.forEach(k => { body[k] = rec[k]; });

    const url = isNewRef.current
      ? `${API_BASE}/v6/record?app=${APP_ID}&table=${tCode}`
      : `${API_BASE}/v6/record?app=${APP_ID}&table=${tCode}&id=${rec.r_auto}`;

    try {
      const res  = await apiFetch(url, {
        method:'POST', headers:{ 'Content-Type':'application/json' },
        body: JSON.stringify(body),
      });
      const data = await res.json();
      if (data.status !== 'ok') throw new Error(data.message);

      if (isNewRef.current) {
        setIsNew(false);
        onNewChange && onNewChange(false);
        fetchRecord(data.r_auto);
      } else {
        setSaved(true);
        setDirty(false);
        pendingRef.current = {};
        onSavedChange  && onSavedChange(true);
        onDirtyChange  && onDirtyChange(false);
        setTimeout(() => { setSaved(false); onSavedChange && onSavedChange(false); }, 2000);
      }
    } catch(err) {
      setError(`Save: ${err.message}`);
    } finally {
      setSaving(false);
      onSavingChange && onSavingChange(false);
    }
  }, [tCode, fetchRecord]);

  // ── Initial load ──────────────────────────────
  React.useEffect(() => {
    if (!isNew) fetchRecord(recordId);
    else setLoading(false);
  }, [recordId]);

  // ── Field change ──────────────────────────────
  const handleChange = (field, value) => {
    setDirty(true);
    onDirtyChange && onDirtyChange(true);
    const updated = { ...recordRef.current, [field]: value };
    setRecord(updated);
    recordRef.current = updated;
    onRecordChange && onRecordChange(updated);

    if (saveMode === 'autosave') {
      pendingRef.current[field] = value;
      clearTimeout(timeoutRef.current);
      timeoutRef.current = setTimeout(() => {
        if (Object.keys(pendingRef.current).length > 0) {
          saveRecord(recordRef.current, { ...pendingRef.current });
          pendingRef.current = {};
        }
      }, appSettings?.timeout || 3000);
    }
  };

  const handleBlur = (field) => {
    if (saveMode === 'autosave' && pendingRef.current[field] !== undefined) {
      clearTimeout(timeoutRef.current);
      saveRecord(recordRef.current, { ...pendingRef.current });
      pendingRef.current = {};
    }
  };

  // ── Navigation ────────────────────────────────
  const curPos  = foundSet?.indexOf(recordId) ?? -1;
  const handlePrev = React.useCallback(() => {
    if (curPos > 0) setRecordId(foundSet[curPos - 1]);
  }, [foundSet, curPos]);
  const handleNext = React.useCallback(() => {
    if (curPos < (foundSet?.length||0) - 1) setRecordId(foundSet[curPos + 1]);
  }, [foundSet, curPos]);
  const handleNew  = React.useCallback(() => {
    setRecord({}); setIsNew(true); setDirty(false);
    onNewChange && onNewChange(true);
    setLoading(false);
  }, []);
  const handleSave = React.useCallback(() => {
    saveRecord(recordRef.current);
  }, [saveRecord]);
  const handleDelete = React.useCallback(async () => {
    if (!record || isNew) return;
    if (!window.confirm('Delete this record?\n\nThis can be recovered later from Trash.')) return;
    try {
      const res  = await apiFetch(
        `${API_BASE}/v6/record?app=${APP_ID}&table=${tCode}&id=${record.r_auto}`,
        { method:'DELETE' }
      );
      const data = await res.json();
      if (data.status !== 'ok') throw new Error(data.message);
      const pos    = foundSet?.indexOf(record.r_auto) ?? -1;
      if (foundSet?.length > 1) {
        setRecordId(foundSet[pos + 1] ?? foundSet[pos - 1]);
      } else {
        onBack && onBack();
      }
    } catch(err) { alert(`Delete failed: ${err.message}`); }
  }, [record, isNew, foundSet, tCode]);

  const handleDuplicate = React.useCallback(async () => {
    if (!record || isNew) return;
    try {
      const body = {};
      Object.keys(record).forEach(k => { if (/^f\d+$/.test(k)) body[k] = record[k]; });
      const res  = await apiFetch(
        `${API_BASE}/v6/record?app=${APP_ID}&table=${tCode}`,
        { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body) }
      );
      const data = await res.json();
      if (data.status !== 'ok') throw new Error(data.message);
      fetchRecord(data.r_auto);
    } catch(err) { alert(`Duplicate failed: ${err.message}`); }
  }, [record, isNew, tCode, fetchRecord]);

  React.useImperativeHandle(ref, () => ({
    goFirst:      () => { if (foundSet?.length > 0) setRecordId(foundSet[0]); },
    goPrev:       handlePrev,
    goNext:       handleNext,
    goLast:       () => { if (foundSet?.length > 0) setRecordId(foundSet[foundSet.length-1]); },
    refresh:      () => fetchRecord(recordId),
    newRecord:    handleNew,
    save:         handleSave,
    deleteRecord: handleDelete,
    duplicate:    handleDuplicate,
  }));

  // ── Effective theme — layout overrides app theme ──
  const effectiveTheme = React.useMemo(() => {
    if (!detail) return theme || {};
    const overrides = {};
    ['nav_bg','nav_color','font','font_size','page_bg'].forEach(k => {
      if (detail[k]) overrides[k] = detail[k];
    });
    return { ...(theme || {}), ...overrides };
  }, [theme, detail]);

  // ── Resolve nav_data template ─────────────────
  // e.g. "{f1} {f2}" → "John Smith"
  function resolveNavData() {
    if (!detail?.nav_data || !record) return null;
    return detail.nav_data.replace(/\{(f\d+)\}/g, (_, k) => record[k] || '');
  }

  // ── Render ────────────────────────────────────
  const pages  = detail?.layout || [];
  const curPage = pages.find(p => String(p.id) === String(activePage)) || pages[0];

  return (
    <div style={{ fontFamily: effectiveTheme.font,
                  fontSize: effectiveTheme.font_size ? `${effectiveTheme.font_size}px` : undefined }}>

      {/* ── Status bars ── */}
      {isNew && (
        <div style={SDV.newBar}>✨ New record — fill in fields and save</div>
      )}
      {dirty && saveMode === 'manual' && !isNew && (
        <div style={SDV.dirtyBar}>⚠ Unsaved changes</div>
      )}
      {error && (
        <div style={SDV.errorBar}>
          ❌ {error}
          <button onClick={() => setError(null)} style={SDV.errorClose}>✕</button>
        </div>
      )}

      {/* ── Search panel ── */}
      {searchOpen && (
        <div style={SDV.searchPanel}>
          <div style={{ display:'flex', gap:12, flexWrap:'wrap', alignItems:'flex-end' }}>
            <div>
              <div style={SDV.searchLabel}>Search All Fields</div>
              <input value={searchGlobal}
                onChange={e => setSearchGlobal(e.target.value)}
                onKeyDown={e => e.key === 'Enter' && handleSearch()}
                placeholder={`Search ${tableRow?.name || tCode}…`}
                style={SDV.searchInput} />
            </div>
            <div style={{ fontSize:11, color:'#94a3b8', paddingBottom:4 }}>— or —</div>
            <div>
              <div style={SDV.searchLabel}>Field</div>
              <select value={searchField} onChange={e => setSearchField(e.target.value)}
                style={SDV.searchSelect}>
                <option value="">Select field…</option>
                {schemaFields.map(f => (
                  <option key={f.id} value={f.code}>{f.name}</option>
                ))}
              </select>
            </div>
            <div>
              <div style={SDV.searchLabel}>Condition</div>
              <select value={searchOp} onChange={e => setSearchOp(e.target.value)}
                style={SDV.searchSelect}>
                <option value="contains">Contains</option>
                <option value="equals">Equals</option>
                <option value="starts">Starts with</option>
                <option value="empty">Is empty</option>
                <option value="notempty">Is not empty</option>
              </select>
            </div>
            <div>
              <div style={SDV.searchLabel}>Value</div>
              <input value={searchValue}
                onChange={e => setSearchValue(e.target.value)}
                onKeyDown={e => e.key === 'Enter' && handleSearch()}
                placeholder="Search value…"
                style={SDV.searchInput} />
            </div>
            <div style={{ display:'flex', gap:8 }}>
              <button onClick={handleSearch} disabled={searching}
                style={SDV.searchBtn}>{searching ? '…' : 'Search'}</button>
              <button onClick={clearSearch} style={SDV.clearBtn}>Clear</button>
              <button onClick={onSearchClose} style={SDV.clearBtn}>✕</button>
            </div>
          </div>
          {searchMsg && (
            <div style={{ marginTop:8, fontSize:12,
                          color: searchMsg.includes('found') ? '#2563eb' : '#dc2626' }}>
              {searchMsg}
            </div>
          )}
        </div>
      )}

      {/* ── Card wrapper ── */}
      <div>

        {/* ── Skeleton on first load ── */}
        {loading && !isNew && !record && <DetailSkeleton />}

        {/* ── Main content ── */}
        {record !== null && (() => {
          const hasTabs   = pages.length > 1;
          const tabStyle  = detail?.tab_style  || 'underline';
          const tabSticky = detail?.tab_sticky !== false;

          return (
            <>
              {/* Tabs — outside card */}
              {hasTabs && (
                <TabBar
                  pages={pages}
                  activeId={curPage?.id}
                  tabStyle={tabStyle}
                  tabSticky={tabSticky}
                  theme={effectiveTheme}
                  onSelect={id => setActivePage(id)}
                />
              )}

              {/* Card — wraps rows/cols/content only */}
              <div className="do-card" style={{
                background:   effectiveTheme.card_bg || '#ffffff',
                borderRadius: effectiveTheme.border_radius ? `${effectiveTheme.border_radius}px` : '8px',
                boxShadow:    '0 1px 4px rgba(0,0,0,0.06)',
              }}>
                {hasTabs && curPage && (curPage.rows || []).length === 0 && (
                  <div style={{ padding:'40px 20px', textAlign:'center',
                                color:'#cbd5e1', fontSize:13 }}>
                    No content on this page yet
                  </div>
                )}
                {(curPage?.rows || []).map(row => (
                  <RowRenderer key={row.id}
                    row={row}
                    objects={objects}
                    record={record}
                    onChange={handleChange}
                    onBlur={handleBlur}
                    onForceSave={() => saveRecord(recordRef.current)}
                    valueListMap={valueListMap}
                    appId={APP_ID}
                    tCode={tCode}
                    recordId={recordId}
                    appData={appData}
                    onSaved={() => {
                      setSaved(true);
                      setTimeout(() => setSaved(false), 2000);
                      // Re-fetch record so uploaded files appear immediately
                      fetchRecord(recordId);
                    }}
                    apiKeys={apiKeys}
                  />
                ))}
              </div>
            </>
          );
        })()}
      </div>

      {/* ── Meta bar — shown if detail.show_meta !== false ── */}
      {record && detail?.show_meta !== false && (
        <div style={SDV.metaBar}>
          <span>r_auto: <b>{record.r_auto}</b></span>
          <span>r_id: <b style={{ fontFamily:'DM Mono, monospace', fontSize:11 }}>{record.r_id}</b></span>
          {record.r_date_insert && <span>inserted: <b>{new Date(record.r_date_insert).toLocaleString()}</b></span>}
          {record.r_date_update && <span>updated: <b>{new Date(record.r_date_update).toLocaleString()}</b></span>}
          {record.r_user_insert && <span>by: <b>{record.r_user_insert}</b></span>}
          {record.r_user_update && <span>updated by: <b>{record.r_user_update}</b></span>}
        </div>
      )}

      {/* ── DO Dev panels — IP restricted ── */}
      {isDoDev() && record && (<>

        {/* API Log */}
        <div style={SDV.devPanel}>
          <div style={SDV.devPanelHdr} onClick={() => setLogOpen(o => !o)}>
            <span>▾ 🔌 API LOG</span>
            <span style={{ fontSize:11, opacity:0.6 }}>{apiLog.length} entries</span>
          </div>
          {logOpen && (
            <div style={SDV.devPanelBody}>
              {apiLog.length === 0 && (
                <div style={{ color:'#64748b', fontSize:11 }}>No log entries yet</div>
              )}
              {[...apiLog].reverse().map((entry, i) => (
                <div key={i} style={{ color: entry.type==='ok' ? '#4ade80' : '#67e8f9',
                                      fontSize:11, fontFamily:'DM Mono, monospace',
                                      marginBottom:2 }}>
                  {entry.ts} — {entry.type==='ok' ? '✅' : '—'} {entry.msg}
                </div>
              ))}
            </div>
          )}
        </div>

        {/* JWT panel */}
        <div style={SDV.devPanel}>
          <div style={SDV.devPanelHdr} onClick={() => setJwtOpen(o => !o)}>
            <span>▾ 🔑 JWT DATA</span>
          </div>
          {jwtOpen && (() => {
            const raw = localStorage.getItem(window.jwtKey ? window.jwtKey() : '');
            let decoded = null;
            if (raw) {
              try {
                const payload = raw.split('.')[1];
                decoded = JSON.parse(atob(payload + '=='.slice((payload.length % 4) || 4)));
              } catch(e) {}
            }
            return (
              <div style={SDV.devPanelBody}>
                {!decoded && <div style={{ color:'#f87171', fontSize:11 }}>No JWT found</div>}
                {decoded && Object.entries(decoded).map(([k, v]) => (
                  <div key={k} style={{ fontSize:11, fontFamily:'DM Mono, monospace',
                                        color:'#e2e8f0', marginBottom:3 }}>
                    <span style={{ color:'#94a3b8' }}>{k}:</span>{' '}
                    <span style={{ color:'#67e8f9' }}>
                      {typeof v === 'object' ? JSON.stringify(v) : String(v)}
                    </span>
                  </div>
                ))}
                <div style={{ marginTop:8, borderTop:'1px solid #334155', paddingTop:8 }}>
                  <div style={{ fontSize:10, color:'#64748b', marginBottom:4 }}>RAW TOKEN</div>
                  <div style={{ fontSize:9, color:'#475569', fontFamily:'DM Mono, monospace',
                                wordBreak:'break-all', lineHeight:1.4 }}>
                    {raw || 'none'}
                  </div>
                </div>
              </div>
            );
          })()}
        </div>
      </>)}

    </div>
  );
});

// ─────────────────────────────────────────────
// Styles — prefix SDV (Detail View V7)
// ─────────────────────────────────────────────
const SDV = {
  newBar:     { background:'#f0f9ff', border:'1px solid #bae6fd', borderRadius:6,
                padding:'8px 14px', marginBottom:12, fontSize:13, color:'#0369a1' },
  dirtyBar:   { background:'#fffbeb', border:'1px solid #fde68a', borderRadius:6,
                padding:'8px 14px', marginBottom:12, fontSize:13, color:'#92400e' },
  errorBar:   { background:'#fef2f2', border:'1px solid #fecaca', borderRadius:6,
                padding:'8px 14px', marginBottom:12, fontSize:13, color:'#991b1b',
                display:'flex', alignItems:'center', justifyContent:'space-between' },
  errorClose: { background:'none', border:'none', color:'#991b1b',
                cursor:'pointer', fontSize:16, padding:0 },
  searchPanel:  { background:'#f8fafc', border:'1px solid #e2e8f0', borderRadius:8,
                  padding:'14px 16px', marginBottom:12 },
  searchLabel:  { fontSize:10, fontWeight:700, color:'#94a3b8', textTransform:'uppercase',
                  letterSpacing:'0.06em', marginBottom:4 },
  searchInput:  { padding:'7px 10px', border:'1.5px solid #e2e8f0', borderRadius:6,
                  fontSize:13, fontFamily:'DM Sans, sans-serif', outline:'none',
                  minWidth:180, display:'block' },
  searchSelect: { padding:'7px 10px', border:'1.5px solid #e2e8f0', borderRadius:6,
                  fontSize:13, fontFamily:'DM Sans, sans-serif', background:'#fff',
                  outline:'none' },
  searchBtn:    { padding:'7px 16px', background:'#0f1923', color:'#fff', border:'none',
                  borderRadius:6, fontSize:13, fontWeight:600, cursor:'pointer',
                  fontFamily:'DM Sans, sans-serif' },
  clearBtn:     { padding:'7px 12px', background:'#fff', color:'#374151',
                  border:'1.5px solid #e2e8f0', borderRadius:6, fontSize:13,
                  cursor:'pointer', fontFamily:'DM Sans, sans-serif' },
  metaBar:      { display:'flex', flexWrap:'wrap', gap:'6px 20px', padding:'8px 14px',
                  fontSize:11, color:'#94a3b8', background:'#f8fafc',
                  borderTop:'1px solid #e2e8f0', marginTop:4, borderRadius:'0 0 8px 8px',
                  fontFamily:'DM Sans, sans-serif' },
  devPanel:     { marginTop:8, background:'#0f172a', borderRadius:8,
                  border:'1px solid #1e293b', overflow:'hidden' },
  devPanelHdr:  { display:'flex', justifyContent:'space-between', alignItems:'center',
                  padding:'8px 14px', color:'#94a3b8', fontSize:11, fontWeight:700,
                  textTransform:'uppercase', letterSpacing:'0.06em',
                  cursor:'pointer', userSelect:'none',
                  fontFamily:'DM Mono, monospace' },
  devPanelBody: { padding:'10px 14px', maxHeight:200, overflowY:'auto' },
};
