// ─────────────────────────────────────────────
// DO_DetailView.jsx  
// Main layout engine, RenderObject, and record view
//
// Depends on (load before this in index.html):
//   DO_Shared.jsx          — constants, styles, helpers
//   DO_Fields_Text.jsx     — TextareaField, NumberField
//   DO_Fields_DateTime.jsx — DateField, TimeField, DateTimeField
//   DO_Fields_Choice.jsx   — SelectField
//   DO_Fields_Display.jsx  — TextField, Skeleton
//   DO_Fields_Rating.jsx   — RatingField
//   DO_UploadFields.jsx    — ImageField, GalleryField, FileField, CameraField
// ─────────────────────────────────────────────

const { useState, useEffect, useCallback, useRef } = React;

function RenderObject({ obj, value, record, onChange, onBlur, onForceSave, valueListMap, appId, table, recordId, appData, onSaved, apiKeys }) {
  const props = { obj, value, onChange, onBlur, valueListMap };
  // Build bucket URL — regional endpoint required for bucket names with dots
  // Clean URLs via CloudFlare CNAME → do-east.s3.amazonaws.com
  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, value, appId, table, recordId, bucket, onSaved };
  switch (obj.type) {
    case "input":
    case "textarea":  return <TextareaField  {...props} />;
    case "number":    return <NumberField    {...props} />;
    case "date":      return <DateField      {...props} />;
    case "time":      return <TimeField      {...props} />;
    case "datetime":  return <DateTimeField  {...props} />;
    case "select":
    case "checkbox":
    case "radio":     return <SelectField    {...props} />;
    case "text":      return <TextField obj={obj} record={record} />;
    case "rating":    return <RatingField obj={obj} record={record} onChange={onChange} onForceSave={onForceSave} />;
    case "divider":   return (
      <div style={{ borderTop: `${obj.height||1}px solid ${obj.color||'#e2e8f0'}`, margin:'8px 0' }} />
    );
    case "spacer":    return (
      <div style={{ height: (obj.height||20)+'px' }} />
    );
    case "tabs":      return null; // rendered as tab bar above grid, not inline
    // Unified file type — display attribute controls rendering
    // Legacy type:"image" and type:"gallery" still supported as aliases
    case "file":
    case "image":
    case "gallery": {
      const display = obj.display || obj.type; // "image" | "gallery" | "files"
      if (display === "image")   return <ImageField   {...uploadProps} />;
      if (display === "gallery") return <GalleryField {...uploadProps} />;
      return <FileField {...uploadProps} />;
    }
    case "camera":    return <CameraField  {...uploadProps} />;
    case "portal":    return <PortalField obj={obj} appId={appId} table={table} recordId={recordId} appData={appData} />;
    case "map":       return <MapField obj={obj} value={value} record={record} apiKeys={apiKeys} />;
    default:          return <div style={S.unknown}>? {obj.type}</div>;
  }
}

// ─────────────────────────────────────────────
// SKELETON
// ─────────────────────────────────────────────
function Skeleton() {
  return (
    <div style={S.skeletonWrap}>
      {[1, 2].map(i => (
        <div key={i} style={S.skeletonRow}>
          {[25, 25, 50].map((w, j) => (
            <div key={j} style={{ ...S.skeletonCol, width: `${w}%` }}>
              <div style={S.skeletonLabel} />
              <div style={S.skeletonInput} />
              <div style={{ ...S.skeletonLabel, width: "60%", marginTop: 14 }} />
              <div style={S.skeletonInput} />
            </div>
          ))}
        </div>
      ))}
    </div>
  );
}

// ─────────────────────────────────────────────
// SEARCH PANEL
// Searches all schema fields for the current table,
// calls onFoundSetChange with found r_auto ids,
// and navigates to the first result.
// ─────────────────────────────────────────────
function SearchPanel({ tableProp, allObjects, onFoundSetChange, onSearchClose, onNavigate, log }) {
  const [term,  setTerm]  = React.useState('');
  const [note,  setNote]  = React.useState('');
  const [error, setError] = React.useState('');
  const [busy,  setBusy]  = React.useState(false);

  const doSearch = React.useCallback(async () => {
    const q = term.trim();
    if (!q) return;
    setError(''); setNote('Searching...');
    setBusy(true);

    const tbl = tableProp || (typeof TABLE !== 'undefined' ? TABLE : '');
    const schemaFields = (allObjects || [])
      .filter(o => o.group === 'schema' && o.type === 'field' && o.table === tbl && o.status !== '0')
      .map(o => o.field)
      .filter(Boolean);
    const searchFields = schemaFields.length > 0 ? schemaFields.join(',') : 'f1';

    try {
      const params = new URLSearchParams({
        app:          APP_ID,
        table:        tbl,
        fields:       'r_auto',
        limit:        5000,
        offset:       0,
        sort:         'r_auto',
        dir:          'asc',
        search:       q,
        searchfields: searchFields,
      });
      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 === 0) {
        setNote(`No records found for "${q}"`);
        setBusy(false);
        return;
      }
      onFoundSetChange && onFoundSetChange(ids, data.total);
      onNavigate && onNavigate(ids[0]);
      onSearchClose && onSearchClose();
      log && log(`🔍 "${q}" — ${ids.length} record(s) found`);
    } catch (err) {
      setError(`Search failed: ${err.message}`);
      log && log(`❌ Search: ${err.message}`);
    } finally {
      setBusy(false);
    }
  }, [term, tableProp, allObjects]);

  return (
    <div style={S.searchPanel}>
      <div style={S.searchRow}>
        <input
          style={S.searchInput}
          placeholder="Search records..."
          autoFocus
          value={term}
          onChange={e => { setTerm(e.target.value); setNote(''); setError(''); }}
          onKeyDown={e => {
            if (e.key === 'Enter') doSearch();
            if (e.key === 'Escape') onSearchClose && onSearchClose();
          }}
        />
        <button style={S.searchBtn} onClick={doSearch} disabled={busy}>
          {busy ? '…' : '🔍 Search'}
        </button>
        <button style={{ ...S.searchBtn, background: '#64748b' }} onClick={() => onSearchClose && onSearchClose()}>
          ✕
        </button>
      </div>
      {note  && <div style={S.searchNote}>{note}</div>}
      {error && <div style={{ ...S.searchNote, color: '#ef4444' }}>{error}</div>}
    </div>
  );
}

// ─────────────────────────────────────────────
// MAIN COMPONENT
// ─────────────────────────────────────────────
const DODetailView = React.forwardRef(function DODetailView({
  allObjects, appSettings: propSettings, startId, isNewRecord,
  table: tableProp,
  foundSet, foundTotal, onFoundSetChange, onBack, onTableSelect, theme = {},
  onRecordChange, onSavingChange, onSavedChange, onDirtyChange, onLoadingChange, onNewChange,
  searchOpen, onSearchClose,
} = {}, ref) {

  // Build layout — use defined layout or fall back to default from schema fields
  const initLayout = () => {
    if (!allObjects) return null;
    const tbl = tableProp || TABLE;
    // Filter objects for this table:
    // - layout rows/cols: pass if they have matching table attr OR no table attr (legacy)
    // - objects: must match current table
    // - valuelists: always pass through
    const filtered = allObjects.filter(o => {
      if (o.group === "layout") return !o.table || o.table === tbl;
      if (o.group === "object") return o.table === tbl;
      if (o.group === "valuelist") return true;
      return false;
    });
    const defined = buildLayout(filtered);
    if (defined && defined.length > 0) return defined;
    // No layout defined — fall back to default from schema fields
    return buildDefaultLayout(allObjects, tbl);
  };
  const [layout,       setLayout]       = useState(initLayout);
  const [valueListMap, setValueListMap] = useState(allObjects ? buildValueListMap(allObjects) : {});

  // Extract API keys from allObjects (group=app, type=apikeys)
  // Used by MapField and future API-dependent objects
  const apiKeys = React.useMemo(() => {
    if (!allObjects) return {};
    const obj = allObjects.find(o => o.group === 'app' && o.type === 'apikeys');
    if (!obj) return {};
    // Keys are spread directly on the object (google_maps:{api_key:...}, etc.)
    return obj;
  }, [allObjects]);
  const [appSettings,  setAppSettings]  = useState(propSettings || { save: "manual", timeout: 3000 });
  const [record,       setRecord]       = useState(isNewRecord ? {} : null);
  const [recordId,     setRecordId]     = useState(startId || 1);
  const [isNew,        setIsNew]        = useState(isNewRecord || false);
  const [totalRecords, setTotalRecords] = useState(null);
  const [loading,      setLoading]      = useState(true);
  const [saving,       setSaving]       = useState(false);
  const [saved,        setSaved]        = useState(false);
  const [dirty,        setDirty]        = useState(false);
  const [error,        setError]        = useState(null);
  const [apiLog,       setApiLog]       = useState([]);
  const [activeTab,    setActiveTab]    = useState(null); // active tab id — null = show all

  const windowWidth = useWindowWidth();
  const bp          = getBreakpoint(windowWidth);

  // Layout-level settings — read from appSettings or window globals
  // Defaults: fixed container, M density
  const containerMode = appSettings?.container || window.APP_CONTAINER || 'fixed';
  const density       = appSettings?.density   || window.APP_DENSITY   || 'M';
  const D             = DENSITY_SCALE[density] || DENSITY_SCALE['M'];

  const recordRef   = useRef(record);
  const dirtyRef    = useRef(dirty);
  const settingsRef = useRef(appSettings);
  const isNewRef    = useRef(isNew);
  const timeoutRef  = useRef(null);
  const pendingRef  = useRef({});

  recordRef.current   = record;
  dirtyRef.current    = dirty;
  settingsRef.current = appSettings;
  isNewRef.current    = isNew;

  const log = (msg) =>
    setApiLog(prev =>
      [`${new Date().toLocaleTimeString()} — ${msg}`, ...prev].slice(0, 12)
    );

  // ─────────────────────────────────────────────
  // FETCH RECORD
  // Sets recordId in sync so nav works correctly
  // ─────────────────────────────────────────────
  const fetchRecord = useCallback((id) => {
    // DON'T clear record here — keep previous record visible while loading
    // This prevents the flash of empty fields on Next/Prev
    setLoading(true);
    setDirty(false);
    setIsNew(false);
    onNewChange && onNewChange(false);
    setError(null);
    pendingRef.current = {};
    log(`Fetching record id=${id}...`);

    apiFetch(`${API_BASE}/v6/record?app=${APP_ID}&table=${tableProp || TABLE}&id=${id}`)
      .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
      .then(data => {
        if (data.status !== "ok") throw new Error(data.message);
        // Swap record only when new data is ready — no flash
        setRecord(data.record);
        setRecordId(data.record.r_auto);
        setLoading(false);
        onRecordChange && onRecordChange(data.record);
        onLoadingChange && onLoadingChange(false);
        log(`✅ Record r_auto=${data.record.r_auto}`);
      })
      .catch(err => {
        setError(`Record: ${err.message}`);
        setLoading(false);
        onLoadingChange && onLoadingChange(false);
        log(`❌ ${err.message}`);
      });
  }, []);

  // ─────────────────────────────────────────────
  // SAVE — INSERT or UPDATE
  // After INSERT calls fetchRecord directly —
  // avoids React state timing issues with useEffect
  // ─────────────────────────────────────────────
  const saveRecord = 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 === 0) return;

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

    // No id in URL = INSERT, id in URL = UPDATE
    const url = isNewRef.current
      ? `${API_BASE}/v6/record?app=${APP_ID}&table=${tableProp || TABLE}`
      : `${API_BASE}/v6/record?app=${APP_ID}&table=${tableProp || TABLE}&id=${rec.r_auto}`;

    log(`${isNewRef.current ? "Creating" : fields ? "Autosaving" : "Saving"} — ${keys.join(", ")}`);

    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) {
        // ── INSERT success ──
        // Set isNew false FIRST, then fetch the new record directly
        // Do NOT use setRecordId + rely on useEffect — timing is unreliable
        setIsNew(false); onNewChange && onNewChange(false);
        setTotalRecords(prev => (prev || 0) + 1);
        log(`✅ Created — r_auto=${data.r_auto}`);
        fetchRecord(data.r_auto);  // direct call, no timing issues
      } else {
        // ── UPDATE success ──
        setSaved(true);
        setDirty(false);
        pendingRef.current = {};
        onSavedChange && onSavedChange(true);
        onDirtyChange && onDirtyChange(false);
        log(`✅ Saved — ${keys.length} field(s)`);
        setTimeout(() => setSaved(false), 2000);
      }
    } catch(err) {
      setError(`Save: ${err.message}`);
      log(`❌ Save: ${err.message}`);
    } finally {
      setSaving(false);
      onSavingChange && onSavingChange(false);
    }
  }, [fetchRecord]);

  // ─────────────────────────────────────────────
  // FETCH LAYOUT
  // ─────────────────────────────────────────────
  useEffect(() => {
    if (allObjects) { log("Layout from App.jsx"); return; }
    log("Fetching layout...");
    apiFetch(`${API_BASE}/v6/layout?app=${APP_ID}`)
      .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
      .then(data => {
        if (data.status !== "ok") throw new Error(data.message);
        const settings = buildAppSettings(data.layout);
        setLayout(buildLayout(data.layout));
        setValueListMap(buildValueListMap(data.layout));
        setAppSettings(settings);
        log(`✅ Layout — ${data.count} objects | save: ${settings.save}`);
      })
      .catch(err => { setError(`Layout: ${err.message}`); log(`❌ ${err.message}`); });
  }, []);

  // ─────────────────────────────────────────────
  // FETCH RECORD COUNT
  // ─────────────────────────────────────────────
  useEffect(() => {
    apiFetch(`${API_BASE}/v6/records?app=${APP_ID}&table=${tableProp || TABLE}&limit=1`)
      .then(r => r.json())
      .then(data => { if (data.status === "ok") setTotalRecords(data.total); })
      .catch(() => {});
  }, []);

  // ─────────────────────────────────────────────
  // INITIAL RECORD LOAD
  // Only runs on mount and when recordId changes
  // from navigation — NOT after new record insert
  // ─────────────────────────────────────────────
  useEffect(() => {
    if (!isNew) fetchRecord(recordId);
    else if (isNewRecord) setLoading(false);
  }, [recordId]);  // intentionally omit isNew — fetchRecord handles it directly

  // Reset active tab when table changes
  useEffect(() => { setActiveTab(null); }, [tableProp]);

  // ─────────────────────────────────────────────
  // FIELD CHANGE
  // ─────────────────────────────────────────────
  const handleChange = (field, value) => {
    setDirty(true);
    setSaved(false);
    onDirtyChange && onDirtyChange(true);
    onSavedChange && onSavedChange(false);
    const updatedRecord = { ...recordRef.current, [field]: value };
    setRecord(updatedRecord);
    pendingRef.current[field] = true;

    if (settingsRef.current.save === "timeout") {
      if (timeoutRef.current) clearTimeout(timeoutRef.current);
      timeoutRef.current = setTimeout(() => {
        if (dirtyRef.current) {
          saveRecord(updatedRecord, { ...pendingRef.current });
        }
      }, settingsRef.current.timeout);
    }
  };

  // ─────────────────────────────────────────────
  // FIELD BLUR — autosave
  // ─────────────────────────────────────────────
  const handleBlur = useCallback(() => {
    if (settingsRef.current.save === "autosave" && dirtyRef.current) {
      saveRecord(recordRef.current, { ...pendingRef.current });
    }
  }, [saveRecord]);

  // ── Force save — for objects with no blur event (e.g. Rating)
  // Accepts the field+value directly because setState is async —
  // recordRef.current hasn't updated yet when this fires after onChange
  const handleForceSave = useCallback((field, value) => {
    const updatedRecord = { ...recordRef.current, [field]: value };
    const pending       = { ...pendingRef.current, [field]: true };
    saveRecord(updatedRecord, pending);
  }, [saveRecord]);

  // ── Manual save ──
  const handleSave = () => {
    if ((record || isNew) && dirty) saveRecord(record, null);
  };

  // ── Navigation — uses found set if available ──
  const currentPos = foundSet?.length > 0 ? foundSet.indexOf(record?.r_auto) : -1;
  const hasPrev    = foundSet?.length > 0 ? currentPos > 0 : (record?.r_auto > 1);
  const hasNext    = foundSet?.length > 0 ? currentPos < foundSet.length - 1 : true;

  const posLabel = foundSet?.length > 0 && currentPos >= 0
    ? `${currentPos + 1} of ${foundSet.length}`
    : (record && foundTotal ? `#${record.r_auto} of ${foundTotal}` : null);

  const handlePrev = () => {
    if (foundSet?.length > 0 && currentPos > 0) {
      setRecordId(foundSet[currentPos - 1]);
    } else if (!foundSet?.length && record?.r_auto > 1) {
      setRecordId(record.r_auto - 1);
    }
  };

  const handleNext = () => {
    if (foundSet?.length > 0 && currentPos < foundSet.length - 1) {
      setRecordId(foundSet[currentPos + 1]);
    } else if (!foundSet?.length) {
      setRecordId((record?.r_auto || 1) + 1);
    }
  };

  // ── New record ──
  const handleNew = () => {
    setRecord({});
    setIsNew(true);
    onNewChange && onNewChange(true);
    setDirty(false);
    setError(null);
    setLoading(false);
    pendingRef.current = {};
    log("New record — enter data, autosaves on field exit");
  };

  const saveMode = appSettings.save;

  // Expose methods to parent (App.jsx nav bar)
  // Placed here so all handlers are already defined
  // ── DELETE RECORD ────────────────────────────
  const handleDelete = useCallback(async () => {
    if (!record || isNew) return;
    const confirmed = window.confirm(
      `Delete this record?\n\nThis can be recovered later from Trash.`
    );
    if (!confirmed) return;
    try {
      const tbl = tableProp || TABLE;
      const res = await apiFetch(
        `${API_BASE}/v6/record?app=${APP_ID}&table=${tbl}&id=${record.r_auto}`,
        { method: 'DELETE' }
      );
      const data = await res.json();
      if (data.status !== 'ok') throw new Error(data.message);
      // Navigate to next record in found set, or prev, or go home
      const pos = foundSet?.indexOf(record.r_auto) ?? -1;
      if (foundSet?.length > 1) {
        const nextId = foundSet[pos + 1] ?? foundSet[pos - 1];
        setRecordId(nextId);
      } else {
        onTableSelect && onTableSelect(tbl); // go back to list
      }
    } catch (err) {
      alert(`Delete failed: ${err.message}`);
    }
  }, [record, isNew, foundSet, tableProp]);

  // ── DUPLICATE RECORD ─────────────────────────
  const handleDuplicate = useCallback(async () => {
    if (!record || isNew) return;
    try {
      const tbl = tableProp || TABLE;
      // Build body from all f fields in the current record
      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=${tbl}`,
        {
          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);
      // Navigate to the new duplicate record
      fetchRecord(data.r_auto);
    } catch (err) {
      alert(`Duplicate failed: ${err.message}`);
    }
  }, [record, isNew, tableProp, 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,
  }));

  return (
    <div style={{ ...S.page, background: theme.page_bg, fontFamily: theme.font, fontSize: theme.font_size ? `${theme.font_size}px` : undefined }}>

      {/* ── STATUS BARS — new record hint + unsaved warning ── */}
      {isNew && (
        <div style={S.newBar}>✨ New record — fill in fields and tab to save automatically</div>
      )}
      {dirty && saveMode === "manual" && !isNew && (
        <div style={S.dirtyBar}>⚠ Unsaved changes</div>
      )}

      {/* ── SEARCH PANEL (detail view) ── */}
      {searchOpen && (
        <SearchPanel
          tableProp={tableProp}
          allObjects={allObjects}
          onFoundSetChange={onFoundSetChange}
          onSearchClose={onSearchClose}
          onNavigate={setRecordId}
          log={log}
        />
      )}

      {/* ── ERROR ── */}
      {error && (
        <div style={S.errorBar}>
          ❌ {error}
          <button style={S.errorClose} onClick={() => setError(null)}>✕</button>
        </div>
      )}

      <div style={{
        ...S.container,
        maxWidth:  containerMode === 'fluid' ? '100%' : 1200,
        padding:   '0',
        marginTop: bp === 'xs' || bp === 'sm' ? 8 : 20,
      }}>

        {/* ── SKELETON — first load only, when no record exists yet ── */}
        {loading && !isNew && !record && <Skeleton />}

        {/* ── TABS BAR + GRID ── */}
        {layout && record !== null && (() => {
          // Extract tabs object for THIS table only
          // Must filter by table to avoid showing another table's tabs
          const tbl = tableProp || TABLE;
          const tabsObjFromAll  = (allObjects || []).find(o =>
            o.type === 'tabs' && o.group === 'object' && o.table === tbl
          );
          const tabsObjFromRows = layout.flatMap(r => r.cols || [])
            .flatMap(c => c.objects || []).find(o => o.type === 'tabs');
          const tabsObj   = tabsObjFromAll || tabsObjFromRows;
          const tabDefs   = tabsObj?.tabDefs || [];
          const hastabs   = tabDefs.length > 0;
          const curTab    = activeTab || (hastabs ? String(tabDefs[0]?.id) : null);
          const isUnderline = !tabsObj?.style || tabsObj.style === 'underline';
          const isPills     = tabsObj?.style === 'pills';
          const isTabs      = tabsObj?.style === 'tabs';

          // Rows to render — exclude row 0, filter by layout attribute matching active tab
          // If tabs present and no rows assigned to this tab, show empty placeholder (not fallback)
          const nonZeroRows  = layout.filter(r => String(r.row) !== '0');
          const visibleRows  = hastabs
            ? nonZeroRows.filter(r => String(r.layout) === String(curTab))
            : nonZeroRows;

          return (
            <>
              {/* Tab bar */}
              {hastabs && (
                <div style={{
                  display: 'flex', alignItems: 'center',
                  gap: isPills ? 6 : 0,
                  padding: isPills ? '8px 0 6px' : isUnderline ? '0' : '8px 0 0',
                  borderBottom: isUnderline ? '2px solid #e2e8f0' : 'none',
                  marginBottom: isTabs ? 0 : 8,
                  flexWrap: 'wrap',
                }}>
                  {tabDefs.map(t => {
                    const active = String(t.id) === String(curTab);
                    if (isPills) return (
                      <button key={t.id} onClick={() => setActiveTab(t.id)}
                        style={{ padding:'6px 16px', borderRadius:20, border:'none', fontSize:'1rem',
                          fontWeight:600, cursor:'pointer', fontFamily:'inherit',
                          background: active ? '#2563eb' : '#f1f5f9',
                          color: active ? '#fff' : '#64748b',
                          transition: 'all 0.15s' }}>
                        {t.label}
                      </button>
                    );
                    if (isTabs) return (
                      <button key={t.id} onClick={() => setActiveTab(t.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' }}>
                        {t.label}
                      </button>
                    );
                    // underline (default)
                    return (
                      <button key={t.id} onClick={() => setActiveTab(t.id)}
                        style={{ padding:'10px 18px', border:'none',
                          borderBottom: active ? '2px solid #2563eb' : '2px solid transparent',
                          background: 'none', fontSize:'1rem', fontWeight: active ? 700 : 500,
                          cursor: 'pointer', fontFamily: 'inherit', marginBottom: '-2px',
                          color: active ? '#2563eb' : '#64748b',
                          transition: 'color 0.15s' }}>
                        {t.label}
                      </button>
                    );
                  })}
                </div>
              )}

              {/* Empty tab placeholder */}
              {hastabs && visibleRows.length === 0 && (
                <div style={{
                  padding: '40px 20px', textAlign: 'center',
                  color: '#cbd5e1', fontSize: 13,
                  border: '2px dashed #f1f5f9', borderRadius: 8, margin: '12px 0',
                }}>
                  No content on this tab yet
                </div>
              )}

              {/* Row grid */}
              {visibleRows.map((row, rIdx) => (
                <div key={`row-${rIdx}`} style={{ ...S.row, marginBottom: D.rowGap, flexWrap: bp === 'xs' || bp === 'sm' || bp === 'md' ? 'wrap' : 'nowrap' }}>
                  {row.cols.map((col, cIdx) => (
                    <div
                      key={`col-${rIdx}-${cIdx}`}
                      style={{
                        ...S.col,
                        width:       responsiveWidth(col.width, bp),
                        flex:        (bp === 'xs' || bp === 'sm') ? '1 0 100%' : 'none',
                        padding:     D.colPad,
                        borderRight: (bp === 'xs' || bp === 'sm')
                          ? 'none'
                          : cIdx < row.cols.length - 1 && col.border !== false ? '1px solid #e2e8f0' : 'none',
                        borderBottom: (bp === 'xs' || bp === 'sm') && cIdx < row.cols.length - 1 && col.border !== false
                          ? '1px solid #e2e8f0'
                          : 'none',
                      }}
                    >
                      {col.objects.length === 0
                        ? <div style={S.emptyCol}>—</div>
                        : col.objects.map(obj => (
                          <RenderObject
                            key={obj.id || `${obj.field}-${obj.sort}`}
                            obj={obj}
                            value={record[obj.field]}
                            record={record}
                            onChange={handleChange}
                            onBlur={handleBlur}
                            onForceSave={handleForceSave}
                            valueListMap={valueListMap}
                            appId={APP_ID}
                            table={tableProp || TABLE}
                            recordId={record.r_auto}
                            appData={typeof APP_DATA !== 'undefined' ? APP_DATA : 'va.us'}
                            onSaved={(field) => fetchRecord(recordId)}
                            apiKeys={apiKeys}
                          />
                        ))
                      }
                    </div>
                  ))}
                </div>
              ))}
            </>
          );
        })()}

        {/* ── RECORD META ── */}
        {record && !loading && !isNew && (
          <div style={S.meta}>
            <span>r_auto: <strong>{record.r_auto}</strong></span>
            <span>r_id: <strong style={{fontSize:10}}>{record.r_id}</strong></span>
            <span>inserted: <strong>{record.r_date_insert}</strong></span>
            <span>updated: <strong>{record.r_date_update}</strong></span>
          </div>
        )}

        {/* ── API LOG ── */}
        <details style={S.logPanel}>
          <summary style={S.logSummary}>🔌 API Log</summary>
          <div style={S.logBody}>
            {apiLog.map((entry, i) => (
              <div key={i} style={S.logEntry}>{entry}</div>
            ))}
          </div>
        </details>

      </div>
    </div>
  );
});

// ─────────────────────────────────────────────
// STYLES
// ─────────────────────────────────────────────
const NAVY   = "#1a3a5c";
const BORDER = "#e2e8f0";
// ── Responsive hook ─────────────────────────
function useWindowWidth() {
  const [width, setWidth] = React.useState(window.innerWidth);
  React.useEffect(() => {
    const handler = () => setWidth(window.innerWidth);
    window.addEventListener('resize', handler);
    return () => window.removeEventListener('resize', handler);
  }, []);
  return width;
}

// Breakpoints (Bootstrap-style)
// xs < 576, sm 576-767, md 768-991, lg 992-1199, xl 1200+
function getBreakpoint(w) {
  if (w < 576)  return 'xs';
  if (w < 768)  return 'sm';
  if (w < 992)  return 'md';
  if (w < 1200) return 'lg';
  return 'xl';
}

// Responsive col width — collapses on small screens
// Each col stores a width 1-12 (Bootstrap style)
function responsiveWidth(colWidth, bp) {
  const w = parseInt(colWidth) || 12;
  if (bp === 'xs') return '100%';    // portrait phone — always stack
  if (bp === 'sm') return '100%';    // landscape phone — stack too, use swipe to navigate
  if (bp === 'md') {                 // tablet — 2 col max, must add to 100%
    if (w >= 7) return '100%';       // wide cols full width
    if (w >= 4) return '50%';        // medium cols side by side
    return '33.333%';                // narrow cols 3-up
  }
  return `${Math.round((w / 12) * 100)}%`;  // desktop — exact width
}

// Density padding scale
const DENSITY_SCALE = {
  S:  { colPad: '8px 10px',  rowGap: 2,  labelSize: '9px',  inputPad: '4px 8px',  fieldGap: 8  },
  M:  { colPad: '14px 16px', rowGap: 3,  labelSize: '10px', inputPad: '6px 10px', fieldGap: 14 },
  L:  { colPad: '18px 20px', rowGap: 6,  labelSize: '11px', inputPad: '8px 12px', fieldGap: 18 },
  XL: { colPad: '24px 28px', rowGap: 10, labelSize: '12px', inputPad: '10px 14px',fieldGap: 24 },
};

const BG     = "#f1f5f9";
const TEXT   = "#1e293b";
const LABEL  = "#64748b";
const CHECK  = "#2563a8";

const S = {
  page:       { fontFamily: "'Segoe UI', sans-serif", background: BG, minHeight: "100vh", fontSize: "15px" },
  nav:        { background: NAVY, color: "#fff", display: "flex", alignItems: "center", padding: "0 24px", height: 52, gap: 12, boxShadow: "0 2px 8px rgba(0,0,0,0.2)", position: "sticky", top: 0, zIndex: 100 },
  navLogo:    { fontSize: 20, fontWeight: 900, letterSpacing: "-0.5px" },
  navCenter:  { display: "flex", alignItems: "center", gap: 8, flex: 1 },
  navTable:   { fontSize: 14, fontWeight: 700 },
  navSep:     { color: "rgba(255,255,255,0.3)" },
  navRecord:  { fontSize: 13, color: "rgba(255,255,255,0.65)" },
  navRight:   { display: "flex", alignItems: "center", gap: 6 },
  navCounter: { fontSize: 11, color: "rgba(255,255,255,0.5)", padding: "0 8px" },
  navDivider: { color: "rgba(255,255,255,0.2)", margin: "0 4px" },
  navBtn:     { background: "rgba(255,255,255,0.1)", border: "1px solid rgba(255,255,255,0.15)", color: "#fff", padding: "5px 12px", borderRadius: 4, cursor: "pointer", fontSize: 12, fontFamily: "inherit" },
  newBadge:       { fontSize: 10, fontWeight: 700, background: "#22c55e", color: "#fff", padding: "2px 8px", borderRadius: 10, letterSpacing: "0.05em" },
  saveModeTag:    { fontSize: 10, color: "rgba(255,255,255,0.4)", padding: "2px 8px", border: "1px solid rgba(255,255,255,0.15)", borderRadius: 10 },
  saveBtn:        { background: "#f59e0b", border: "none", color: "#1a1a1a", padding: "6px 18px", borderRadius: 4, cursor: "pointer", fontSize: 13, fontWeight: 700, fontFamily: "inherit" },
  saveBtnSaved:   { background: "#22c55e", color: "#fff" },
  saveBtnSaving:  { background: "#94a3b8", cursor: "wait" },
  saveBtnDisabled:{ background: "#94a3b8", cursor: "default", opacity: 0.6 },
  newBar:     { background: "#dcfce7", color: "#166534", fontSize: 12, fontWeight: 600, padding: "6px 24px", borderBottom: "1px solid #bbf7d0" },
  dirtyBar:   { background: "#fef3c7", color: "#92400e", fontSize: 12, fontWeight: 600, padding: "6px 24px", borderBottom: "1px solid #fde68a" },
  errorBar:   { background: "#fee2e2", color: "#991b1b", fontSize: 12, fontWeight: 600, padding: "8px 24px", display: "flex", alignItems: "center", justifyContent: "space-between" },
  errorClose: { background: "none", border: "none", cursor: "pointer", color: "#991b1b", fontSize: 16 },
  container:  { margin: "20px auto" },
  row:        { display: "flex", background: "#fff", border: `1px solid ${BORDER}`, borderRadius: 6, marginBottom: 3 },
  col:        { boxSizing: "border-box", minWidth: 0 },
  emptyCol:   { color: "#cbd5e1", fontSize: 12 },
  fieldWrap:  { marginBottom: 14 },
  label:      { display: "block", fontSize: 10, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.07em", color: LABEL, marginBottom: 4 },
  input:      { width: "100%", boxSizing: "border-box", border: `1px solid ${BORDER}`, borderRadius: 5, padding: "6px 10px", fontSize: "1em", color: TEXT, background: "#fafbfc", outline: "none", fontFamily: "inherit", transition: "border-color 0.15s", display: "block" },
  fieldHint:  { fontSize: 10, color: LABEL, marginTop: 3 },
  fieldAffix: { fontSize: 13, color: LABEL, whiteSpace: "nowrap" },
  optionGroup:{ display: "flex", gap: 10, flexWrap: "wrap", paddingTop: 4 },
  optionLabel:{ display: "flex", alignItems: "center", gap: 6, cursor: "pointer", userSelect: "none" },
  optionText: { fontSize: "0.93em", color: TEXT },
  customCheck:{ width: 16, height: 16, border: `2px solid ${BORDER}`, background: "#fafbfc", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, transition: "all 0.15s", cursor: "pointer" },
  customCheckOn:{ background: CHECK, borderColor: CHECK },
  checkMark:  { color: "#fff", fontSize: 11, fontWeight: 700, lineHeight: 1 },
  radioDot:   { width: 6, height: 6, borderRadius: "50%", background: "#fff", display: "block" },
  textObj:    { padding: "10px 14px", borderRadius: 5, fontSize: "1em", fontWeight: 600, marginBottom: 12 },
  unknown:    { fontSize: 11, color: "#f87171", fontStyle: "italic", padding: "4px 0" },
  meta:       { display: "flex", gap: 20, flexWrap: "wrap", marginTop: 12, padding: "8px 12px", background: "#fff", border: `1px solid ${BORDER}`, borderRadius: 6, fontSize: 11, color: LABEL },
  skeletonWrap: { display: "flex", flexDirection: "column", gap: 3 },
  skeletonRow:  { display: "flex", background: "#fff", border: `1px solid ${BORDER}`, borderRadius: 6 },
  skeletonCol:  { padding: "14px 16px", borderRight: `1px solid ${BORDER}` },
  skeletonLabel:{ height: 10, background: "#e2e8f0", borderRadius: 4, marginBottom: 8, width: "40%" },
  skeletonInput:{ height: 34, background: "#f1f5f9", borderRadius: 5 },
  searchPanel:{ background: "#fff", border: "1px solid #e2e8f0", borderRadius: 8, padding: "12px 16px", margin: "0 0 8px 0" },
  searchRow:  { display: "flex", gap: 8, alignItems: "center" },
  searchInput:{ flex: 1, border: "1px solid #e2e8f0", borderRadius: 5, padding: "6px 10px", fontSize: 13, fontFamily: "inherit", outline: "none" },
  searchBtn:  { background: "#1a3a5c", color: "#fff", border: "none", borderRadius: 5, padding: "6px 14px", cursor: "pointer", fontSize: 12, fontFamily: "inherit", whiteSpace: "nowrap" },
  searchNote: { fontSize: 11, color: "#94a3b8", marginTop: 8 },
  logPanel:   { marginTop: 16, background: "#0f172a", borderRadius: 8, overflow: "hidden" },
  logSummary: { color: "#64748b", fontSize: 11, fontWeight: 600, textTransform: "uppercase", letterSpacing: "0.08em", padding: "10px 16px", cursor: "pointer" },
  logBody:    { padding: "0 16px 16px" },
  logEntry:   { color: "#7dd3fc", fontSize: 11, lineHeight: 1.8, borderBottom: "1px solid #1e293b", padding: "2px 0" },
};
