import { useState, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { apiClient } from '../../core/api/api-client'
import { ExtensionTimeoutAction, SessionMode, ApiErrorCode } from '../../core/constants'

const fetchAnonymityConfig = async () => {
  const res = await apiClient.get('/internal/config/anonymity')
  return res.data.data
}

const updateAnonymityConfig = async (anonymity_enabled) => {
  const res = await apiClient.patch('/internal/config/anonymity', { anonymity_enabled })
  return res.data.data
}

const fetchMaxCustomersConfig = async () => {
  const res = await apiClient.get('/internal/config/max-customers-per-mitra')
  return res.data.data
}

const updateMaxCustomersConfig = async (max_customers_per_mitra) => {
  const res = await apiClient.patch('/internal/config/max-customers-per-mitra', { max_customers_per_mitra })
  return res.data.data
}

const fetchExtensionTimeoutConfig = async () => {
  const res = await apiClient.get('/internal/config/extension-timeout')
  return res.data.data
}

const updateExtensionTimeoutConfig = async (extension_timeout_seconds) => {
  const res = await apiClient.patch('/internal/config/extension-timeout', { extension_timeout_seconds })
  return res.data.data
}

// Phase 3.1: Mitra Ping Config
const fetchMitraPingConfig = async () => {
  const res = await apiClient.get('/internal/config/mitra-ping')
  return res.data.data
}

const updateMitraPingConfig = async (data) => {
  const res = await apiClient.patch('/internal/config/mitra-ping', data)
  return res.data.data
}

const fetchEarlyEndConfig = async () => {
  const res = await apiClient.get('/internal/config/early-end')
  return res.data.data
}

const updateEarlyEndConfig = async (data) => {
  const res = await apiClient.patch('/internal/config/early-end', data)
  return res.data.data
}

// Phase 3.3: Topic Sensitivity
const fetchSensitivityConfig = async () => {
  const res = await apiClient.get('/internal/config/sensitivity')
  return res.data.data
}

const updateSensitivityConfig = async (data) => {
  const res = await apiClient.patch('/internal/config/sensitivity', data)
  return res.data.data
}

// Paid pairing flow + extension flip
const fetchPairingBlastTimeout = async () => {
  const res = await apiClient.get('/internal/config/pairing-blast-timeout')
  return res.data.data
}

const updatePairingBlastTimeout = async (pairing_blast_timeout_seconds) => {
  const res = await apiClient.patch('/internal/config/pairing-blast-timeout', { pairing_blast_timeout_seconds })
  return res.data.data
}

const fetchPaymentSessionTimeout = async () => {
  const res = await apiClient.get('/internal/config/payment-session-timeout')
  return res.data.data
}

const updatePaymentSessionTimeout = async (payment_session_timeout_minutes) => {
  const res = await apiClient.patch('/internal/config/payment-session-timeout', { payment_session_timeout_minutes })
  return res.data.data
}

const fetchReturningChatTimeout = async () => {
  const res = await apiClient.get('/internal/config/returning-chat-timeout')
  return res.data.data
}

const updateReturningChatTimeout = async (returning_chat_confirmation_timeout_seconds) => {
  const res = await apiClient.patch('/internal/config/returning-chat-timeout', { returning_chat_confirmation_timeout_seconds })
  return res.data.data
}

const fetchExtensionDefaultAction = async () => {
  const res = await apiClient.get('/internal/config/extension-default-action')
  return res.data.data
}

const updateExtensionDefaultAction = async (extension_default_action_on_timeout) => {
  const res = await apiClient.patch('/internal/config/extension-default-action', { extension_default_action_on_timeout })
  return res.data.data
}

// Phase 4: First-session discount
const fetchFirstSessionDiscount = async () => {
  const res = await apiClient.get('/internal/config/first-session-discount')
  return res.data.data
}
const updateFirstSessionDiscount = async (patch) => {
  const res = await apiClient.patch('/internal/config/first-session-discount', patch)
  return res.data.data
}

// Phase 4 / Stage 4: Per-row CRUD for pricing tiers (relational migration).
// GET shape: { chat: [{id, mode, minutes, price_idr, original_price_idr, tag,
//   sort_order, is_active, updated_at, created_at}, ...], call: [...] }
const fetchPricingTiers = async () => {
  const res = await apiClient.get('/internal/config/pricing-tiers')
  return res.data.data
}
const createPricingTier = async (body) => {
  const res = await apiClient.post('/internal/config/pricing-tiers', body)
  return res.data.data
}
const patchPricingTier = async ({ id, ...body }) => {
  const res = await apiClient.patch(`/internal/config/pricing-tiers/${id}`, body)
  return res.data.data
}
// Soft-delete: backend sets is_active=false. updated_at is the optimistic-lock token.
const deletePricingTier = async ({ id, updated_at }) => {
  const res = await apiClient.delete(`/internal/config/pricing-tiers/${id}`, {
    data: { updated_at },
  })
  return res.data.data
}

// Phase 4: Support handles
const fetchSupportHandles = async () => {
  const res = await apiClient.get('/internal/config/support-handles')
  return res.data.data
}
const updateSupportHandles = async (patch) => {
  const res = await apiClient.patch('/internal/config/support-handles', patch)
  return res.data.data
}

// Test OTP bypass allowlist — phone-scoped static OTPs for Apple reviewers / QA.
// Backend rejects requestOtp() to Fazpass for these phones; plaintext OTP is
// bcrypt-hashed on save and never readable after.
const fetchTestOtpBypass = async () => {
  const res = await apiClient.get('/internal/config/test-otp-bypass')
  return res.data.data
}
const setTestOtpBypassEnabled = async (enabled) => {
  const res = await apiClient.patch('/internal/config/test-otp-bypass/enabled', { enabled })
  return res.data.data
}
const addTestOtpBypassEntry = async (body) => {
  const res = await apiClient.post('/internal/config/test-otp-bypass/entries', body)
  return res.data.data
}
const updateTestOtpBypassEntry = async ({ id, ...patch }) => {
  const res = await apiClient.patch(`/internal/config/test-otp-bypass/entries/${id}`, patch)
  return res.data.data
}
const deleteTestOtpBypassEntry = async (id) => {
  const res = await apiClient.delete(`/internal/config/test-otp-bypass/entries/${id}`)
  return res.data.data
}

export default function SettingsPage() {
  const queryClient = useQueryClient()
  const { data, isLoading } = useQuery({ queryKey: ['config-anonymity'], queryFn: fetchAnonymityConfig })

  const anonymityMutation = useMutation({
    mutationFn: updateAnonymityConfig,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-anonymity'] }),
  })

  const { data: maxData, isLoading: maxLoading } = useQuery({
    queryKey: ['config-max-customers'],
    queryFn: fetchMaxCustomersConfig,
  })

  const maxMutation = useMutation({
    mutationFn: updateMaxCustomersConfig,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-max-customers'] }),
  })

  // Phase 3: Extension Timeout
  const { data: etData, isLoading: etLoading } = useQuery({
    queryKey: ['config-extension-timeout'],
    queryFn: fetchExtensionTimeoutConfig,
  })
  const etMutation = useMutation({
    mutationFn: updateExtensionTimeoutConfig,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-extension-timeout'] }),
  })

  // Phase 3: Early End
  const { data: eeData, isLoading: eeLoading } = useQuery({
    queryKey: ['config-early-end'],
    queryFn: fetchEarlyEndConfig,
  })
  const eeMutation = useMutation({
    mutationFn: updateEarlyEndConfig,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-early-end'] }),
  })

  // Phase 3.1: Mitra Ping
  const { data: mpData, isLoading: mpLoading } = useQuery({
    queryKey: ['config-mitra-ping'],
    queryFn: fetchMitraPingConfig,
  })
  const mpMutation = useMutation({
    mutationFn: updateMitraPingConfig,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-mitra-ping'] }),
  })

  // Phase 3.3: Topic Sensitivity
  const { data: senData, isLoading: senLoading } = useQuery({
    queryKey: ['config-sensitivity'],
    queryFn: fetchSensitivityConfig,
  })
  const senMutation = useMutation({
    mutationFn: updateSensitivityConfig,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-sensitivity'] }),
  })

  // Pairing Blast Timeout
  const { data: pbtData, isLoading: pbtLoading } = useQuery({
    queryKey: ['config-pairing-blast-timeout'],
    queryFn: fetchPairingBlastTimeout,
  })
  const pbtMutation = useMutation({
    mutationFn: updatePairingBlastTimeout,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-pairing-blast-timeout'] }),
  })

  // Payment Session Timeout
  const { data: pstData, isLoading: pstLoading } = useQuery({
    queryKey: ['config-payment-session-timeout'],
    queryFn: fetchPaymentSessionTimeout,
  })
  const pstMutation = useMutation({
    mutationFn: updatePaymentSessionTimeout,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-payment-session-timeout'] }),
  })

  // Returning Chat Confirmation Timeout
  const { data: rctData, isLoading: rctLoading } = useQuery({
    queryKey: ['config-returning-chat-timeout'],
    queryFn: fetchReturningChatTimeout,
  })
  const rctMutation = useMutation({
    mutationFn: updateReturningChatTimeout,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-returning-chat-timeout'] }),
  })

  // Extension Default Action on Timeout
  const { data: edaData, isLoading: edaLoading } = useQuery({
    queryKey: ['config-extension-default-action'],
    queryFn: fetchExtensionDefaultAction,
  })
  const edaMutation = useMutation({
    mutationFn: updateExtensionDefaultAction,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-extension-default-action'] }),
  })

  // Stale-write toast surfaced from any of the per-row pricing mutations.
  // Plain string for now — settings page has no toast lib; the banner sits above the section.
  const [pricingToast, setPricingToast] = useState(null)

  const handlePricingError = (err, { onRefetch }) => {
    const code = err?.response?.data?.error?.code
    const msg = err?.response?.data?.error?.message
    if (code === ApiErrorCode.STALE_WRITE || code === ApiErrorCode.NOT_FOUND) {
      setPricingToast('Someone else just edited this. Refreshing...')
      onRefetch?.()
      return
    }
    if (code === ApiErrorCode.VALIDATION) {
      setPricingToast(msg ? `Validation: ${msg}` : 'Validation failed.')
      return
    }
    setPricingToast(msg || 'Failed to save.')
  }

  // Phase 4 / Stage 4: First-session discount with optimistic locking.
  const { data: fsdData, isLoading: fsdLoading, refetch: refetchFsd } = useQuery({
    queryKey: ['config-first-session-discount'],
    queryFn: fetchFirstSessionDiscount,
  })
  const fsdMutation = useMutation({
    mutationFn: updateFirstSessionDiscount,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-first-session-discount'] }),
    onError: (err) => handlePricingError(err, { onRefetch: refetchFsd }),
  })

  // Phase 4 / Stage 4: Pricing tiers — per-row CRUD with optimistic locking.
  const { data: ptData, isLoading: ptLoading, refetch: refetchTiers } = useQuery({
    queryKey: ['config-pricing-tiers'],
    queryFn: fetchPricingTiers,
  })
  const invalidateTiers = () => queryClient.invalidateQueries({ queryKey: ['config-pricing-tiers'] })
  const ptCreateMutation = useMutation({
    mutationFn: createPricingTier,
    onSuccess: invalidateTiers,
    onError: (err) => handlePricingError(err, { onRefetch: refetchTiers }),
  })
  const ptPatchMutation = useMutation({
    mutationFn: patchPricingTier,
    onSuccess: invalidateTiers,
    onError: (err) => handlePricingError(err, { onRefetch: refetchTiers }),
  })
  const ptDeleteMutation = useMutation({
    mutationFn: deletePricingTier,
    onSuccess: invalidateTiers,
    onError: (err) => handlePricingError(err, { onRefetch: refetchTiers }),
  })

  // Phase 4: Support handles
  const { data: shData, isLoading: shLoading } = useQuery({
    queryKey: ['config-support-handles'],
    queryFn: fetchSupportHandles,
  })
  const shMutation = useMutation({
    mutationFn: updateSupportHandles,
    onSuccess: () => queryClient.invalidateQueries({ queryKey: ['config-support-handles'] }),
  })

  // Test OTP bypass allowlist. Single query, four mutations (enable + CRUD).
  const { data: tobData, isLoading: tobLoading } = useQuery({
    queryKey: ['config-test-otp-bypass'],
    queryFn: fetchTestOtpBypass,
  })
  const invalidateTob = () => queryClient.invalidateQueries({ queryKey: ['config-test-otp-bypass'] })
  const tobEnabledMutation = useMutation({ mutationFn: setTestOtpBypassEnabled, onSuccess: invalidateTob })
  const tobAddMutation = useMutation({ mutationFn: addTestOtpBypassEntry, onSuccess: invalidateTob })
  const tobUpdateMutation = useMutation({ mutationFn: updateTestOtpBypassEntry, onSuccess: invalidateTob })
  const tobDeleteMutation = useMutation({ mutationFn: deleteTestOtpBypassEntry, onSuccess: invalidateTob })

  if (
    isLoading || maxLoading || etLoading || eeLoading || mpLoading || senLoading ||
    pbtLoading || pstLoading || rctLoading || edaLoading ||
    fsdLoading || ptLoading || shLoading || tobLoading
  ) return <div>Loading...</div>

  return (
    <div>
      <h1>Settings</h1>

      <section style={{ marginBottom: 24 }}>
        <h2>Anonymity</h2>
        <p>Ketika dinonaktifkan, pengguna anonim akan diminta mendaftar setelah sesi selesai.</p>
        <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <input
            type="checkbox"
            checked={data?.anonymity_enabled ?? true}
            onChange={e => anonymityMutation.mutate(e.target.checked)}
            disabled={anonymityMutation.isPending}
          />
          Izinkan pengguna anonim
        </label>
        {anonymityMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
      </section>

      <section style={{ marginBottom: 24 }}>
        <h2>Maks Customer per Mitra</h2>
        <p>Jumlah maksimal customer yang bisa ditangani satu Mitra secara bersamaan. Perubahan hanya berlaku untuk chat baru.</p>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <input
            type="number"
            min="1"
            value={maxData?.max_customers_per_mitra ?? 3}
            onChange={e => {
              const val = parseInt(e.target.value, 10)
              if (val >= 1) maxMutation.mutate(val)
            }}
            disabled={maxMutation.isPending}
            style={{ width: 80 }}
          />
          <span>customer</span>
        </div>
        {maxMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
      </section>

      <section style={{ marginBottom: 24 }}>
        <h2>Extension Timeout</h2>
        <p>Waktu tunggu untuk customer memutuskan dan mitra mengkonfirmasi perpanjangan sesi.</p>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <input
            type="number"
            min="10"
            value={etData?.extension_timeout_seconds ?? 60}
            onChange={e => {
              const val = parseInt(e.target.value, 10)
              if (val >= 10) etMutation.mutate(val)
            }}
            disabled={etMutation.isPending}
            style={{ width: 80 }}
          />
          <span>detik</span>
        </div>
        {etMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
      </section>

      <section style={{ marginBottom: 24 }}>
        <h2>Akhiri Sesi Lebih Awal</h2>
        <p>Izinkan mitra dan/atau customer untuk mengakhiri sesi sebelum waktu habis.</p>
        <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
          <input
            type="checkbox"
            checked={eeData?.mitra_enabled ?? false}
            onChange={e => eeMutation.mutate({ mitra_enabled: e.target.checked })}
            disabled={eeMutation.isPending}
          />
          Izinkan Mitra mengakhiri lebih awal
        </label>
        <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <input
            type="checkbox"
            checked={eeData?.customer_enabled ?? false}
            onChange={e => eeMutation.mutate({ customer_enabled: e.target.checked })}
            disabled={eeMutation.isPending}
          />
          Izinkan Customer mengakhiri lebih awal
        </label>
        {eeMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
      </section>

      <section style={{ marginBottom: 24 }}>
        <h2>Mitra Online Status (Ping)</h2>
        <p>Mitra dianggap online selama heartbeat terakhir berusia ≤ ambang batas. Cadence (frekuensi ping aplikasi) di-fix oleh server lewat env var.</p>
        <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
          <input
            type="checkbox"
            checked={mpData?.require_ping ?? true}
            onChange={e => mpMutation.mutate({ require_ping: e.target.checked })}
            disabled={mpMutation.isPending}
          />
          Wajibkan Mitra Ping (Heartbeat)
        </label>
        <p style={{ fontSize: 12, color: '#666', marginBottom: 8 }}>
          Jika dinonaktifkan, mitra akan tetap online tanpa perlu mengirim ping. QC bertanggung jawab atas kualitas layanan mitra.
        </p>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
          <label>Ambang offline (heartbeat terakhir lebih lama dari):</label>
          <input
            type="number"
            min={mpData?.heartbeat_cadence_seconds ?? 30}
            value={mpData?.stale_after_seconds ?? 45}
            onChange={e => {
              const val = parseInt(e.target.value, 10)
              const floor = mpData?.heartbeat_cadence_seconds ?? 30
              if (Number.isFinite(val) && val >= floor) {
                mpMutation.mutate({ stale_after_seconds: val })
              }
            }}
            disabled={mpMutation.isPending}
            style={{ width: 80 }}
          />
          <span>detik</span>
        </div>
        <p style={{ fontSize: 12, color: '#666', marginBottom: 0 }}>
          Cadence ping mitra: <strong>{mpData?.heartbeat_cadence_seconds ?? 30} detik</strong> (server-set via MITRA_HEARTBEAT_CADENCE_SECONDS env). Nilai ambang minimum mengikuti cadence — tidak bisa lebih rendah.
        </p>
        {mpMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
      </section>

      <section style={{ marginBottom: 24 }}>
        <h2>Sensitivitas Topik</h2>
        <p>Konfigurasi untuk fitur penandaan sesi sebagai topik sensitif oleh Mitra.</p>
        <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
          <input
            type="checkbox"
            checked={senData?.flip_confirmation_enabled ?? true}
            onChange={e => senMutation.mutate({ flip_confirmation_enabled: e.target.checked })}
            disabled={senMutation.isPending}
          />
          Aktifkan dialog konfirmasi saat Mitra menandai topik sensitif
        </label>
        <p style={{ fontSize: 12, color: '#666', marginBottom: 8 }}>
          Jika dinonaktifkan, Mitra langsung menandai tanpa dialog konfirmasi.
        </p>
        <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <input
            type="checkbox"
            checked={senData?.one_way_latch ?? false}
            onChange={e => senMutation.mutate({ one_way_latch: e.target.checked })}
            disabled={senMutation.isPending}
          />
          Kunci searah — Mitra tidak bisa membatalkan tanda topik sensitif
        </label>
        <p style={{ fontSize: 12, color: '#666' }}>
          Jika diaktifkan, setelah sesi ditandai sensitif Mitra tidak dapat mengembalikannya ke topik umum.
        </p>
        {senMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
      </section>

      <section style={{ marginBottom: 24 }}>
        <h2>Batas Waktu Blast Pairing</h2>
        <p>Berapa lama backend menunggu mitra menerima sebelum blast dianggap gagal.</p>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <input
            type="number"
            min="5"
            value={pbtData?.pairing_blast_timeout_seconds ?? 60}
            onChange={e => {
              const val = parseInt(e.target.value, 10)
              if (val >= 5) pbtMutation.mutate(val)
            }}
            disabled={pbtMutation.isPending}
            style={{ width: 80 }}
          />
          <span>detik</span>
        </div>
        {pbtMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
      </section>

      <section style={{ marginBottom: 24 }}>
        <h2>Batas Waktu Sesi Pembayaran</h2>
        <p>Sesi pembayaran customer akan kedaluwarsa otomatis setelah durasi ini jika belum dikonfirmasi atau belum menghasilkan chat.</p>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <input
            type="number"
            min="1"
            value={pstData?.payment_session_timeout_minutes ?? 20}
            onChange={e => {
              const val = parseInt(e.target.value, 10)
              if (val >= 1) pstMutation.mutate(val)
            }}
            disabled={pstMutation.isPending}
            style={{ width: 80 }}
          />
          <span>menit</span>
        </div>
        {pstMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
      </section>

      <section style={{ marginBottom: 24 }}>
        <h2>Batas Waktu Konfirmasi Chat Lanjutan</h2>
        <p>Berapa detik bestie punya waktu untuk menerima/menolak permintaan chat lanjutan dari customer sebelum otomatis ditolak.</p>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <input
            type="number"
            min="5"
            value={rctData?.returning_chat_confirmation_timeout_seconds ?? 20}
            onChange={e => {
              const val = parseInt(e.target.value, 10)
              if (val >= 5) rctMutation.mutate(val)
            }}
            disabled={rctMutation.isPending}
            style={{ width: 80 }}
          />
          <span>detik</span>
        </div>
        {rctMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
      </section>

      <section style={{ marginBottom: 24 }}>
        <h2>Aksi Default jika Bestie Tidak Menjawab Extension</h2>
        <p>Jika bestie tidak merespon permintaan extension dalam batas waktu, sistem akan otomatis menerima atau menolak sesuai pilihan ini.</p>
        <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
          <input
            type="radio"
            name="extension_default_action_on_timeout"
            value={ExtensionTimeoutAction.AUTO_APPROVE}
            checked={(edaData?.extension_default_action_on_timeout ?? ExtensionTimeoutAction.AUTO_APPROVE) === ExtensionTimeoutAction.AUTO_APPROVE}
            onChange={() => edaMutation.mutate(ExtensionTimeoutAction.AUTO_APPROVE)}
            disabled={edaMutation.isPending}
          />
          Otomatis disetujui (auto-approve)
        </label>
        <label style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <input
            type="radio"
            name="extension_default_action_on_timeout"
            value={ExtensionTimeoutAction.AUTO_REJECT}
            checked={edaData?.extension_default_action_on_timeout === ExtensionTimeoutAction.AUTO_REJECT}
            onChange={() => edaMutation.mutate(ExtensionTimeoutAction.AUTO_REJECT)}
            disabled={edaMutation.isPending}
          />
          Otomatis ditolak (auto-reject)
        </label>
        {edaMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
      </section>

      {/* Phase 4: First-session discount (Stage 4 — per-row optimistic lock) */}
      <FirstSessionDiscountSection
        data={fsdData}
        mutation={fsdMutation}
        toast={pricingToast}
        onDismissToast={() => setPricingToast(null)}
      />

      {/* Phase 4: Pricing tiers (Stage 4 — per-row CRUD) */}
      <section style={{ marginBottom: 24 }}>
        <h2>Tier Harga (Phase 4)</h2>
        <p>
          Daftar tier untuk chat dan voice call. Setiap baris di-edit terpisah dengan
          optimistic locking — perubahan operator lain akan trigger auto-refresh.
        </p>
        {pricingToast && (
          <div style={{
            marginBottom: 8, padding: 8, background: '#fff3cd', border: '1px solid #ffeeba',
            color: '#856404', borderRadius: 4, display: 'flex', justifyContent: 'space-between',
          }}>
            <span>{pricingToast}</span>
            <button type="button" onClick={() => setPricingToast(null)} style={{ marginLeft: 12 }}>
              Tutup
            </button>
          </div>
        )}
        {[SessionMode.CHAT, SessionMode.CALL].map((mode) => (
          <PricingTierTable
            key={mode}
            mode={mode}
            tiers={ptData?.[mode] ?? []}
            createMutation={ptCreateMutation}
            patchMutation={ptPatchMutation}
            deleteMutation={ptDeleteMutation}
          />
        ))}
      </section>

      {/* Test OTP bypass — Apple reviewer / QA static OTP allowlist */}
      <TestOtpBypassSection
        data={tobData}
        enabledMutation={tobEnabledMutation}
        addMutation={tobAddMutation}
        updateMutation={tobUpdateMutation}
        deleteMutation={tobDeleteMutation}
      />

      {/* Phase 4: Support handles */}
      <section style={{ marginBottom: 24 }}>
        <h2>Support Handles (Tanya Admin)</h2>
        <p>Deeplink WA + Telegram untuk sheet "Tanya Admin" di client_app.</p>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
          <label style={{ width: 90 }}>WA label:</label>
          <input
            type="text"
            defaultValue={shData?.wa?.label ?? 'WhatsApp'}
            onBlur={e => shMutation.mutate({ wa: { label: e.target.value } })}
            disabled={shMutation.isPending}
            style={{ width: 240 }}
          />
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
          <label style={{ width: 90 }}>WA deeplink:</label>
          <input
            type="text"
            defaultValue={shData?.wa?.deeplink ?? ''}
            onBlur={e => shMutation.mutate({ wa: { deeplink: e.target.value } })}
            disabled={shMutation.isPending}
            style={{ width: 360 }}
            placeholder="https://wa.me/62..."
          />
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
          <label style={{ width: 90 }}>TG label:</label>
          <input
            type="text"
            defaultValue={shData?.telegram?.label ?? 'Telegram'}
            onBlur={e => shMutation.mutate({ telegram: { label: e.target.value } })}
            disabled={shMutation.isPending}
            style={{ width: 240 }}
          />
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          <label style={{ width: 90 }}>TG deeplink:</label>
          <input
            type="text"
            defaultValue={shData?.telegram?.deeplink ?? ''}
            onBlur={e => shMutation.mutate({ telegram: { deeplink: e.target.value } })}
            disabled={shMutation.isPending}
            style={{ width: 360 }}
            placeholder="https://t.me/..."
          />
        </div>
        {shMutation.isError && <p style={{ color: 'red' }}>Gagal menyimpan.</p>}
      </section>
    </div>
  )
}

// ============================================================================
// Pricing tiers — per-row table editor (Stage 4)
// ============================================================================
//
// Per-mode table. Each row carries its own `updated_at` token; PATCH/DELETE
// echo it back to the backend so concurrent edits 409 instead of clobbering.
//
// Layout choice: inline-edit rows over modal. Operators here typically tweak one
// price at a time; a modal would add a click without adding clarity.

const formatIdr = (n) => {
  if (n === null || n === undefined || n === '') return ''
  const v = typeof n === 'number' ? n : parseInt(n, 10)
  if (!Number.isFinite(v)) return ''
  return v.toLocaleString('id-ID')
}

const parseIdr = (s) => {
  if (s === null || s === undefined) return null
  const cleaned = String(s).replace(/[^\d-]/g, '')
  if (cleaned === '' || cleaned === '-') return null
  const v = parseInt(cleaned, 10)
  return Number.isFinite(v) ? v : null
}

function PricingTierTable({ mode, tiers, createMutation, patchMutation, deleteMutation }) {
  const [showAdd, setShowAdd] = useState(false)
  const [editingId, setEditingId] = useState(null)

  return (
    <div style={{ marginBottom: 20 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
        <h3 style={{ margin: 0 }}>
          {mode === SessionMode.CHAT ? 'Chat tiers' : 'Voice call tiers'}
        </h3>
        <button type="button" onClick={() => setShowAdd(s => !s)} disabled={createMutation.isPending}>
          {showAdd ? 'Batal tambah' : '+ Tambah tier'}
        </button>
      </div>

      {showAdd && (
        <AddTierForm
          mode={mode}
          onSubmit={(body) => createMutation.mutate(body, {
            onSuccess: () => setShowAdd(false),
          })}
          onCancel={() => setShowAdd(false)}
          isPending={createMutation.isPending}
        />
      )}

      <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
        <thead>
          <tr style={{ background: '#f7f7f7', textAlign: 'left' }}>
            <th style={th}>Menit</th>
            <th style={th}>Harga (IDR)</th>
            <th style={th}>Harga Coret (IDR)</th>
            <th style={th}>Tag</th>
            <th style={th}>Sort</th>
            <th style={th}>Aktif</th>
            <th style={th}>Aksi</th>
          </tr>
        </thead>
        <tbody>
          {tiers.length === 0 && (
            <tr><td colSpan={7} style={{ ...td, color: '#999', fontStyle: 'italic' }}>Belum ada tier.</td></tr>
          )}
          {tiers.map((tier) => (
            editingId === tier.id ? (
              <EditTierRow
                key={tier.id}
                tier={tier}
                onSave={(patch) => patchMutation.mutate(
                  { id: tier.id, updated_at: tier.updated_at, ...patch },
                  { onSuccess: () => setEditingId(null) },
                )}
                onCancel={() => setEditingId(null)}
                isPending={patchMutation.isPending}
              />
            ) : (
              <ReadOnlyTierRow
                key={tier.id}
                tier={tier}
                onEdit={() => setEditingId(tier.id)}
                onDelete={() => {
                  if (!window.confirm(`Soft-delete tier ${tier.minutes} menit (${formatIdr(tier.price_idr)} IDR)?`)) return
                  deleteMutation.mutate({ id: tier.id, updated_at: tier.updated_at })
                }}
                isDeleting={deleteMutation.isPending}
              />
            )
          ))}
        </tbody>
      </table>
    </div>
  )
}

const th = { padding: '6px 8px', borderBottom: '1px solid #ddd' }
const td = { padding: '6px 8px', borderBottom: '1px solid #eee' }

function ReadOnlyTierRow({ tier, onEdit, onDelete, isDeleting }) {
  return (
    <tr style={{ opacity: tier.is_active ? 1 : 0.55 }}>
      <td style={td}>{tier.minutes}</td>
      <td style={td}>{formatIdr(tier.price_idr)}</td>
      <td style={td}>{tier.original_price_idr ? formatIdr(tier.original_price_idr) : <span style={{ color: '#aaa' }}>—</span>}</td>
      <td style={td}>{tier.tag || <span style={{ color: '#aaa' }}>—</span>}</td>
      <td style={td}>{tier.sort_order}</td>
      <td style={td}>{tier.is_active ? 'ya' : 'tidak'}</td>
      <td style={td}>
        <button type="button" onClick={onEdit} style={{ marginRight: 4 }}>Edit</button>
        {tier.is_active && (
          <button type="button" onClick={onDelete} disabled={isDeleting} style={{ color: '#a00' }}>
            Soft-delete
          </button>
        )}
      </td>
    </tr>
  )
}

function EditTierRow({ tier, onSave, onCancel, isPending }) {
  // Local draft state; only fields that differ are sent on submit (PATCH semantics).
  const [priceStr, setPriceStr] = useState(formatIdr(tier.price_idr))
  const [origStr, setOrigStr] = useState(tier.original_price_idr ? formatIdr(tier.original_price_idr) : '')
  const [tag, setTag] = useState(tier.tag ?? '')
  const [sortOrder, setSortOrder] = useState(String(tier.sort_order))
  const [isActive, setIsActive] = useState(tier.is_active)
  const [minutes, setMinutes] = useState(String(tier.minutes))
  const [localError, setLocalError] = useState(null)

  const submit = () => {
    const price = parseIdr(priceStr)
    const orig = origStr.trim() === '' ? null : parseIdr(origStr)
    const so = parseInt(sortOrder, 10)
    const mins = parseInt(minutes, 10)

    if (price === null || price < 0) return setLocalError('Harga wajib diisi dan >= 0.')
    if (orig !== null && orig < price) return setLocalError('Harga coret harus >= harga.')
    if (!Number.isInteger(so)) return setLocalError('Sort order harus integer.')
    if (!Number.isInteger(mins) || mins <= 0) return setLocalError('Menit harus integer positif.')

    // Build minimal patch: only include changed fields. mode/minutes are not patchable
    // server-side (backend PATCH ignores them) so we skip those here.
    const patch = {}
    if (price !== tier.price_idr) patch.price_idr = price
    if (orig !== tier.original_price_idr) patch.original_price_idr = orig
    if ((tag || null) !== (tier.tag || null)) patch.tag = tag.trim() === '' ? null : tag
    if (so !== tier.sort_order) patch.sort_order = so
    if (isActive !== tier.is_active) patch.is_active = isActive

    if (Object.keys(patch).length === 0) {
      onCancel()
      return
    }
    setLocalError(null)
    onSave(patch)
  }

  return (
    <tr style={{ background: '#fffdf3' }}>
      <td style={td}>
        {/* minutes is part of the unique key; backend doesn't support changing it,
            so we render it read-only but show the value for context. */}
        <input
          type="number" min="1" value={minutes}
          onChange={e => setMinutes(e.target.value)}
          style={{ width: 60 }}
          disabled
          title="Menit tidak bisa diubah — hapus dan buat tier baru."
        />
      </td>
      <td style={td}>
        <input
          type="text" inputMode="numeric"
          value={priceStr}
          onChange={e => setPriceStr(formatIdr(parseIdr(e.target.value) ?? ''))}
          style={{ width: 110 }}
          disabled={isPending}
        />
      </td>
      <td style={td}>
        <input
          type="text" inputMode="numeric" placeholder="opsional"
          value={origStr}
          onChange={e => setOrigStr(e.target.value === '' ? '' : formatIdr(parseIdr(e.target.value) ?? ''))}
          style={{ width: 110 }}
          disabled={isPending}
          title="Anchor / strikethrough price (optional). Harus >= harga."
        />
      </td>
      <td style={td}>
        <input
          type="text" value={tag}
          onChange={e => setTag(e.target.value)}
          style={{ width: 110 }}
          disabled={isPending}
        />
      </td>
      <td style={td}>
        <input
          type="number" value={sortOrder}
          onChange={e => setSortOrder(e.target.value)}
          style={{ width: 60 }}
          disabled={isPending}
        />
      </td>
      <td style={td}>
        <input
          type="checkbox" checked={isActive}
          onChange={e => setIsActive(e.target.checked)}
          disabled={isPending}
        />
      </td>
      <td style={td}>
        <button type="button" onClick={submit} disabled={isPending} style={{ marginRight: 4 }}>
          {isPending ? '...' : 'Simpan'}
        </button>
        <button type="button" onClick={onCancel} disabled={isPending}>Batal</button>
        {localError && <div style={{ color: 'red', fontSize: 11, marginTop: 2 }}>{localError}</div>}
      </td>
    </tr>
  )
}

function AddTierForm({ mode, onSubmit, onCancel, isPending }) {
  const [minutes, setMinutes] = useState('')
  const [priceStr, setPriceStr] = useState('')
  const [origStr, setOrigStr] = useState('')
  const [tag, setTag] = useState('')
  const [sortOrder, setSortOrder] = useState('0')
  const [localError, setLocalError] = useState(null)

  const submit = (e) => {
    e.preventDefault()
    const mins = parseInt(minutes, 10)
    const price = parseIdr(priceStr)
    const orig = origStr.trim() === '' ? null : parseIdr(origStr)
    const so = parseInt(sortOrder, 10)

    if (!Number.isInteger(mins) || mins <= 0) return setLocalError('Menit harus integer positif.')
    if (price === null || price < 0) return setLocalError('Harga wajib diisi dan >= 0.')
    if (orig !== null && orig < price) return setLocalError('Harga coret harus >= harga.')
    if (!Number.isInteger(so)) return setLocalError('Sort order harus integer.')

    setLocalError(null)
    onSubmit({
      mode,
      minutes: mins,
      price_idr: price,
      original_price_idr: orig,
      tag: tag.trim() === '' ? null : tag,
      sort_order: so,
    })
  }

  return (
    <form onSubmit={submit} style={{
      padding: 8, marginBottom: 8, background: '#f0f7ff', border: '1px solid #cde',
      display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'flex-end',
    }}>
      <label style={{ display: 'flex', flexDirection: 'column', fontSize: 12 }}>
        Menit
        <input type="number" min="1" value={minutes} onChange={e => setMinutes(e.target.value)}
          style={{ width: 70 }} disabled={isPending} required />
      </label>
      <label style={{ display: 'flex', flexDirection: 'column', fontSize: 12 }}>
        Harga (IDR)
        <input type="text" inputMode="numeric" value={priceStr}
          onChange={e => setPriceStr(formatIdr(parseIdr(e.target.value) ?? ''))}
          style={{ width: 110 }} disabled={isPending} required />
      </label>
      <label style={{ display: 'flex', flexDirection: 'column', fontSize: 12 }}>
        Harga Coret (opsional)
        <input type="text" inputMode="numeric" value={origStr}
          onChange={e => setOrigStr(e.target.value === '' ? '' : formatIdr(parseIdr(e.target.value) ?? ''))}
          style={{ width: 110 }} disabled={isPending}
          title="Anchor / strikethrough price (optional). Harus >= harga." />
      </label>
      <label style={{ display: 'flex', flexDirection: 'column', fontSize: 12 }}>
        Tag
        <input type="text" value={tag} onChange={e => setTag(e.target.value)}
          style={{ width: 110 }} disabled={isPending} placeholder="paling pas" />
      </label>
      <label style={{ display: 'flex', flexDirection: 'column', fontSize: 12 }}>
        Sort
        <input type="number" value={sortOrder} onChange={e => setSortOrder(e.target.value)}
          style={{ width: 60 }} disabled={isPending} />
      </label>
      <button type="submit" disabled={isPending}>{isPending ? '...' : 'Tambah'}</button>
      <button type="button" onClick={onCancel} disabled={isPending}>Batal</button>
      {localError && <div style={{ color: 'red', fontSize: 12, width: '100%' }}>{localError}</div>}
    </form>
  )
}

// ============================================================================
// First-session discount — single-row form with optimistic locking
// ============================================================================
//
// Buffered "edit then Save" UX (not the per-keystroke pattern the old code used)
// because optimistic locking requires us to send a single coherent patch with
// the last-seen `updated_at`. Each field-level mutation would race itself.

function FirstSessionDiscountSection({ data, mutation, toast, onDismissToast }) {
  // Local form state, hydrated from server data. Reset whenever upstream changes
  // (e.g. after a successful PATCH or after a 409 auto-refetch).
  const [enabled, setEnabled] = useState(data?.enabled ?? false)
  const [actualStr, setActualStr] = useState(formatIdr(data?.actual_price_idr ?? 2000))
  const [gimmickStr, setGimmickStr] = useState(data?.gimmick_price_idr ? formatIdr(data.gimmick_price_idr) : '')
  const [duration, setDuration] = useState(String(data?.duration_minutes ?? 12))
  const [modes, setModes] = useState(data?.modes ?? [SessionMode.CHAT])
  const [localError, setLocalError] = useState(null)

  useEffect(() => {
    if (!data) return
    setEnabled(data.enabled ?? false)
    setActualStr(formatIdr(data.actual_price_idr ?? 2000))
    setGimmickStr(data.gimmick_price_idr ? formatIdr(data.gimmick_price_idr) : '')
    setDuration(String(data.duration_minutes ?? 12))
    setModes(data.modes ?? [SessionMode.CHAT])
    setLocalError(null)
  }, [data?.updated_at])

  const toggleMode = (m, checked) => {
    setModes(prev => checked ? Array.from(new Set([...prev, m])) : prev.filter(x => x !== m))
  }

  const save = () => {
    if (!data?.updated_at) return setLocalError('Belum ada data — tunggu fetch awal.')
    const actual = parseIdr(actualStr)
    const gimmick = gimmickStr.trim() === '' ? null : parseIdr(gimmickStr)
    const dur = parseInt(duration, 10)

    if (actual === null || actual < 0) return setLocalError('Harga aktual harus >= 0.')
    if (gimmick !== null && gimmick < actual) return setLocalError('Harga gimik harus >= harga aktual.')
    if (!Number.isInteger(dur) || dur <= 0) return setLocalError('Durasi harus integer > 0.')
    if (modes.length === 0) return setLocalError('Pilih minimal satu mode.')

    setLocalError(null)
    mutation.mutate({
      updated_at: data.updated_at,
      enabled,
      actual_price_idr: actual,
      gimmick_price_idr: gimmick,
      duration_minutes: dur,
      modes,
    })
  }

  return (
    <section style={{ marginBottom: 24 }}>
      <h2>Diskon Sesi Pertama (Phase 4)</h2>
      <p>
        Diskon untuk sesi pertama customer yang sudah terverifikasi nomor HP.
        Menggantikan free trial Phase 3.
      </p>
      {toast && (
        <div style={{
          marginBottom: 8, padding: 8, background: '#fff3cd', border: '1px solid #ffeeba',
          color: '#856404', borderRadius: 4, display: 'flex', justifyContent: 'space-between',
        }}>
          <span>{toast}</span>
          <button type="button" onClick={onDismissToast} style={{ marginLeft: 12 }}>Tutup</button>
        </div>
      )}
      <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
        <input type="checkbox" checked={enabled} onChange={e => setEnabled(e.target.checked)}
          disabled={mutation.isPending} />
        Aktifkan diskon sesi pertama
      </label>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
        <label>Harga aktual (IDR):</label>
        <input type="text" inputMode="numeric" value={actualStr}
          onChange={e => setActualStr(formatIdr(parseIdr(e.target.value) ?? ''))}
          style={{ width: 140 }} disabled={mutation.isPending} />
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
        <label>Harga gimik / coret (IDR):</label>
        <input type="text" inputMode="numeric" value={gimmickStr}
          onChange={e => setGimmickStr(e.target.value === '' ? '' : formatIdr(parseIdr(e.target.value) ?? ''))}
          style={{ width: 140 }} disabled={mutation.isPending}
          title="Optional. Harus >= harga aktual." />
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
        <label>Durasi (menit):</label>
        <input type="number" min="1" value={duration} onChange={e => setDuration(e.target.value)}
          style={{ width: 80 }} disabled={mutation.isPending} />
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 8 }}>
        <span>Mode yang dapat diskon:</span>
        {[SessionMode.CHAT, SessionMode.CALL].map(m => (
          <label key={m} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
            <input type="checkbox" checked={modes.includes(m)}
              onChange={e => toggleMode(m, e.target.checked)}
              disabled={mutation.isPending} />
            {m}
          </label>
        ))}
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <button type="button" onClick={save} disabled={mutation.isPending || !data}>
          {mutation.isPending ? 'Menyimpan...' : 'Simpan'}
        </button>
        {localError && <span style={{ color: 'red', fontSize: 12 }}>{localError}</span>}
      </div>
    </section>
  )
}

// ============================================================================
// Test OTP Bypass — Apple-reviewer / QA static OTP allowlist
// ============================================================================
//
// SECURITY-SENSITIVE: any phone in this list authenticates with a static OTP
// and never receives an SMS. Backend bcrypt-hashes the plaintext on save and
// never returns it again — to rotate an OTP, edit the entry and set a new one.
//
// The kill-switch toggle disables ALL entries instantly without touching the
// list, useful for incidents. Per-entry expires_at provides automatic disable.

const formatExpiresAt = (iso) => {
  if (!iso) return '—'
  const d = new Date(iso)
  if (Number.isNaN(d.getTime())) return iso
  return d.toLocaleString('id-ID', { dateStyle: 'short', timeStyle: 'short' })
}

const toDatetimeLocal = (iso) => {
  // <input type="datetime-local"> wants "YYYY-MM-DDTHH:mm" in local time.
  if (!iso) return ''
  const d = new Date(iso)
  if (Number.isNaN(d.getTime())) return ''
  const pad = (n) => String(n).padStart(2, '0')
  return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`
}

const fromDatetimeLocal = (s) => {
  // Convert local-tz "YYYY-MM-DDTHH:mm" back to ISO. new Date() handles it.
  if (!s) return null
  const d = new Date(s)
  return Number.isNaN(d.getTime()) ? null : d.toISOString()
}

const sectionErrorText = (err) =>
  err?.response?.data?.error?.message || err?.message || 'Gagal menyimpan.'

function TestOtpBypassSection({ data, enabledMutation, addMutation, updateMutation, deleteMutation }) {
  const [showAdd, setShowAdd] = useState(false)
  const [editingId, setEditingId] = useState(null)
  const entries = data?.entries ?? []
  const isExpired = (iso) => {
    const t = new Date(iso).getTime()
    return Number.isFinite(t) && t <= Date.now()
  }

  return (
    <section style={{ marginBottom: 24, border: '1px solid #f0c36d', padding: 12, background: '#fff8e8' }}>
      <h2 style={{ marginTop: 0 }}>Test OTP Bypass (Apple Reviewer / QA)</h2>
      <p style={{ fontSize: 13, color: '#664' }}>
        Daftar nomor HP yang melewati Fazpass dan login dengan OTP statis.
        Untuk reviewer App Store dan tester internal saja —{' '}
        <strong>siapa pun yang tahu pasangan nomor + OTP bisa login sebagai user ini.</strong>{' '}
        Jaga daftarnya kecil, tetapkan <em>expires_at</em>, dan hapus segera setelah pemakaian.
      </p>

      <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
        <input
          type="checkbox"
          checked={data?.enabled ?? false}
          onChange={e => enabledMutation.mutate(e.target.checked)}
          disabled={enabledMutation.isPending}
        />
        <strong>Aktifkan bypass</strong> (kill switch global — matikan untuk menonaktifkan semua entri sekaligus)
      </label>
      {enabledMutation.isError && (
        <p style={{ color: 'red', fontSize: 12 }}>{sectionErrorText(enabledMutation.error)}</p>
      )}

      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
        <h3 style={{ margin: 0, fontSize: 15 }}>Entri ({entries.length})</h3>
        <button type="button" onClick={() => setShowAdd(s => !s)} disabled={addMutation.isPending}>
          {showAdd ? 'Batal tambah' : '+ Tambah entri'}
        </button>
      </div>

      {showAdd && (
        <AddTestOtpBypassForm
          onSubmit={(body) => addMutation.mutate(body, { onSuccess: () => setShowAdd(false) })}
          onCancel={() => setShowAdd(false)}
          isPending={addMutation.isPending}
          serverError={addMutation.isError ? sectionErrorText(addMutation.error) : null}
        />
      )}

      <table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13, marginTop: 8 }}>
        <thead>
          <tr style={{ background: '#f7f7f7', textAlign: 'left' }}>
            <th style={th}>Phone</th>
            <th style={th}>User type</th>
            <th style={th}>Label</th>
            <th style={th}>Expires</th>
            <th style={th}>OTP hash</th>
            <th style={th}>Aksi</th>
          </tr>
        </thead>
        <tbody>
          {entries.length === 0 && (
            <tr><td colSpan={6} style={{ ...td, color: '#999', fontStyle: 'italic' }}>Belum ada entri.</td></tr>
          )}
          {entries.map((entry) => (
            editingId === entry.id ? (
              <EditTestOtpBypassRow
                key={entry.id}
                entry={entry}
                onSave={(patch) => updateMutation.mutate(
                  { id: entry.id, ...patch },
                  { onSuccess: () => setEditingId(null) },
                )}
                onCancel={() => setEditingId(null)}
                isPending={updateMutation.isPending}
                serverError={updateMutation.isError ? sectionErrorText(updateMutation.error) : null}
              />
            ) : (
              <tr key={entry.id} style={{ opacity: isExpired(entry.expires_at) ? 0.5 : 1 }}>
                <td style={td}>{entry.phone}</td>
                <td style={td}>{entry.user_type}</td>
                <td style={td}>{entry.label}</td>
                <td style={td}>
                  {formatExpiresAt(entry.expires_at)}
                  {isExpired(entry.expires_at) && (
                    <span style={{ marginLeft: 6, color: '#a00', fontWeight: 'bold' }}>EXPIRED</span>
                  )}
                </td>
                <td style={{ ...td, fontFamily: 'monospace', fontSize: 11, color: '#888' }}>
                  {entry.otp_hash ? `${entry.otp_hash.slice(0, 12)}…` : '—'}
                </td>
                <td style={td}>
                  <button type="button" onClick={() => setEditingId(entry.id)} style={{ marginRight: 4 }}>
                    Edit
                  </button>
                  <button
                    type="button"
                    onClick={() => {
                      if (!window.confirm(`Hapus entri "${entry.label}" (${entry.phone})?`)) return
                      deleteMutation.mutate(entry.id)
                    }}
                    disabled={deleteMutation.isPending}
                    style={{ color: '#a00' }}
                  >
                    Hapus
                  </button>
                </td>
              </tr>
            )
          ))}
        </tbody>
      </table>
      {deleteMutation.isError && (
        <p style={{ color: 'red', fontSize: 12 }}>{sectionErrorText(deleteMutation.error)}</p>
      )}
    </section>
  )
}

function AddTestOtpBypassForm({ onSubmit, onCancel, isPending, serverError }) {
  const [phone, setPhone] = useState('+62')
  const [userType, setUserType] = useState('customer')
  const [otp, setOtp] = useState('')
  const [label, setLabel] = useState('')
  // Default expiry: 30 days from now.
  const defaultExpiry = (() => {
    const d = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)
    return toDatetimeLocal(d.toISOString())
  })()
  const [expiresAt, setExpiresAt] = useState(defaultExpiry)
  const [localError, setLocalError] = useState(null)

  const submit = (e) => {
    e.preventDefault()
    if (!/^\+[1-9]\d{6,14}$/.test(phone)) return setLocalError('Phone harus format E.164 (mis. +628...).')
    if (!/^\d{4,8}$/.test(otp)) return setLocalError('OTP harus 4-8 digit angka.')
    if (label.trim().length === 0) return setLocalError('Label wajib diisi.')
    const iso = fromDatetimeLocal(expiresAt)
    if (!iso) return setLocalError('Tanggal expires_at tidak valid.')
    if (new Date(iso).getTime() <= Date.now()) return setLocalError('expires_at harus di masa depan.')

    setLocalError(null)
    onSubmit({ phone, user_type: userType, otp, label: label.trim(), expires_at: iso })
  }

  return (
    <form onSubmit={submit} style={{
      padding: 10, marginBottom: 8, background: '#f0f7ff', border: '1px solid #cde',
      display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'flex-end',
    }}>
      <label style={{ display: 'flex', flexDirection: 'column', fontSize: 12 }}>
        Phone (E.164)
        <input type="text" value={phone} onChange={e => setPhone(e.target.value.trim())}
          style={{ width: 180 }} disabled={isPending} placeholder="+628111111111" required />
      </label>
      <label style={{ display: 'flex', flexDirection: 'column', fontSize: 12 }}>
        User type
        <select value={userType} onChange={e => setUserType(e.target.value)}
          disabled={isPending} style={{ width: 110 }}>
          <option value="customer">customer</option>
          <option value="mitra">mitra</option>
        </select>
      </label>
      <label style={{ display: 'flex', flexDirection: 'column', fontSize: 12 }}>
        OTP (4-8 digit)
        <input type="text" inputMode="numeric" value={otp}
          onChange={e => setOtp(e.target.value.replace(/\D/g, '').slice(0, 8))}
          style={{ width: 110, fontFamily: 'monospace' }} disabled={isPending} required />
      </label>
      <label style={{ display: 'flex', flexDirection: 'column', fontSize: 12 }}>
        Label
        <input type="text" value={label} onChange={e => setLabel(e.target.value)}
          style={{ width: 200 }} disabled={isPending} placeholder="Apple Reviewer #1" required />
      </label>
      <label style={{ display: 'flex', flexDirection: 'column', fontSize: 12 }}>
        Expires at
        <input type="datetime-local" value={expiresAt} onChange={e => setExpiresAt(e.target.value)}
          disabled={isPending} required />
      </label>
      <button type="submit" disabled={isPending}>{isPending ? '...' : 'Tambah'}</button>
      <button type="button" onClick={onCancel} disabled={isPending}>Batal</button>
      {(localError || serverError) && (
        <div style={{ color: 'red', fontSize: 12, width: '100%' }}>
          {localError || serverError}
        </div>
      )}
    </form>
  )
}

function EditTestOtpBypassRow({ entry, onSave, onCancel, isPending, serverError }) {
  const [label, setLabel] = useState(entry.label)
  const [expiresAt, setExpiresAt] = useState(toDatetimeLocal(entry.expires_at))
  const [otp, setOtp] = useState('')  // blank = don't rotate
  const [localError, setLocalError] = useState(null)

  const submit = () => {
    const patch = {}
    if (label.trim() !== entry.label) {
      if (label.trim().length === 0) return setLocalError('Label tidak boleh kosong.')
      patch.label = label.trim()
    }
    const iso = fromDatetimeLocal(expiresAt)
    if (!iso) return setLocalError('Tanggal expires_at tidak valid.')
    if (iso !== entry.expires_at) {
      if (new Date(iso).getTime() <= Date.now()) return setLocalError('expires_at harus di masa depan.')
      patch.expires_at = iso
    }
    if (otp.length > 0) {
      if (!/^\d{4,8}$/.test(otp)) return setLocalError('OTP harus 4-8 digit angka (kosongkan untuk tidak ganti).')
      patch.otp = otp
    }
    if (Object.keys(patch).length === 0) {
      onCancel()
      return
    }
    setLocalError(null)
    onSave(patch)
  }

  return (
    <tr style={{ background: '#fffdf3' }}>
      <td style={td}>{entry.phone}</td>
      <td style={td}>{entry.user_type}</td>
      <td style={td}>
        <input type="text" value={label} onChange={e => setLabel(e.target.value)}
          style={{ width: 180 }} disabled={isPending} />
      </td>
      <td style={td}>
        <input type="datetime-local" value={expiresAt} onChange={e => setExpiresAt(e.target.value)}
          disabled={isPending} />
      </td>
      <td style={td}>
        <input
          type="text" inputMode="numeric" value={otp}
          onChange={e => setOtp(e.target.value.replace(/\D/g, '').slice(0, 8))}
          placeholder="Rotate OTP (opsional)"
          style={{ width: 140, fontFamily: 'monospace' }}
          disabled={isPending}
        />
      </td>
      <td style={td}>
        <button type="button" onClick={submit} disabled={isPending} style={{ marginRight: 4 }}>
          {isPending ? '...' : 'Simpan'}
        </button>
        <button type="button" onClick={onCancel} disabled={isPending}>Batal</button>
        {(localError || serverError) && (
          <div style={{ color: 'red', fontSize: 11, marginTop: 2 }}>{localError || serverError}</div>
        )}
      </td>
    </tr>
  )
}
