import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { saveRecording, getQueue, getRecording, removeRecording, clearQueue, updateRecordingStatus, appendChunk, assembleChunks, listOrphanedSessions, clearChunks } from './offlineQueue'
import { StatusBar, Style } from '@capacitor/status-bar'
import Cactus from './lib/cactus'
import { runPipeline } from './lib/pipeline'
import './App.css'

// API_BASE resolves in this order at module-load:
//   1. localStorage 'sakhi_server_url' — user-entered LAN URL, set via the
//      Server URL field below the status line. Required for the Capacitor APK,
//      where window.location.hostname is 'localhost' (the WebView's own scheme)
//      so the default expression would point at the phone's loopback.
//   2. VITE_API_BASE_URL build-time env var — used by CI / pinned builds.
//   3. Default — depends on how the bundle is being served:
//        a. Vite dev server (port 3000 / 5173) → http://<hostname>:8000 to
//           reach the separate FastAPI process.
//        b. Otherwise (FastAPI-served bundle on a workstation, HF Space, any
//           production deploy) → same-origin relative URLs. This is what makes
//           the HF Space work: the Space proxies one HTTPS port, there is no
//           separate :8000 reachable from outside.
function defaultApiBase() {
  const port = typeof window !== 'undefined' ? window.location.port : ''
  if (port === '3000' || port === '5173') {
    return `http://${window.location.hostname}:8000`
  }
  return ''
}
function resolveApiBase() {
  try {
    const stored = typeof localStorage !== 'undefined' ? localStorage.getItem('sakhi_server_url') : null
    if (stored && stored.trim()) return stored.trim().replace(/\/+$/, '')
  } catch (_) {}
  if (import.meta.env.VITE_API_BASE_URL) return import.meta.env.VITE_API_BASE_URL
  return defaultApiBase()
}
const API_BASE = resolveApiBase()
const VISIT_OPTIONS = [
  { label: 'Auto-detect', value: 'auto' },
  { label: 'ANC Visit', value: 'anc_visit' },
  { label: 'PNC Visit', value: 'pnc_visit' },
  { label: 'Delivery', value: 'delivery' },
  { label: 'Child Health', value: 'child_health' },
]

function initialMetadata() {
  const stickyAsha = typeof localStorage !== 'undefined' ? localStorage.getItem('sakhi_asha_id') || '' : ''
  return {
    patient_name: '',
    patient_age: '',
    age_unit: 'years',
    patient_sex: '',
    patient_mobile: '',
    asha_id: stickyAsha,
    visit_date: new Date().toISOString().slice(0, 10),
  }
}

function appendMetadataToFormData(formData, metadata) {
  if (!metadata) return
  for (const [k, v] of Object.entries(metadata)) {
    if (v !== '' && v != null) formData.append(k, String(v))
  }
}

function metadataPayload(metadata) {
  if (!metadata) return null
  const out = {}
  for (const [k, v] of Object.entries(metadata)) {
    if (v === '' || v == null) continue
    out[k] = k === 'patient_age' ? Number(v) : v
  }
  return Object.keys(out).length ? out : null
}

function PatientMetadataHeader({ metadata, setMetadata, visitType, setVisitType }) {
  const update = (k, v) => setMetadata((m) => ({ ...m, [k]: v }))
  return (
    <div className="card metadata-card">
      <h3 style={{ marginTop: 0 }}>Patient &amp; Visit Info</h3>
      <div className="metadata-grid">
        <label>
          <span>ASHA ID</span>
          <input value={metadata.asha_id} onChange={(e) => update('asha_id', e.target.value)} placeholder="e.g. ASHA-1234" />
        </label>
        <label>
          <span>Visit Date</span>
          <input type="date" value={metadata.visit_date} onChange={(e) => update('visit_date', e.target.value)} />
        </label>
        <label>
          <span>Visit Type</span>
          <select value={visitType} onChange={(e) => setVisitType(e.target.value)}>
            {VISIT_OPTIONS.map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
          </select>
        </label>
        <label>
          <span>Patient Name</span>
          <input value={metadata.patient_name} onChange={(e) => update('patient_name', e.target.value)} placeholder="मरीज़ का नाम" />
        </label>
        <label>
          <span>Age</span>
          <div className="age-row">
            <input type="number" min="0" max="120" value={metadata.patient_age} onChange={(e) => update('patient_age', e.target.value)} />
            <select value={metadata.age_unit} onChange={(e) => update('age_unit', e.target.value)}>
              <option value="years">years</option>
              <option value="months">months</option>
            </select>
          </div>
        </label>
        <label>
          <span>Sex</span>
          <select value={metadata.patient_sex} onChange={(e) => update('patient_sex', e.target.value)}>
            <option value="">—</option>
            <option value="female">female</option>
            <option value="male">male</option>
          </select>
        </label>
        <label>
          <span>Mobile</span>
          <input type="tel" value={metadata.patient_mobile} onChange={(e) => update('patient_mobile', e.target.value)} placeholder="10-digit (optional)" />
        </label>
      </div>
    </div>
  )
}

const VOICE_STAGE_META = {
  asr: 'Transcribing audio...',
  normalize: 'Normalizing Hindi numbers...',
  detect: 'Detecting visit type...',
  form: 'Extracting structured form...',
  danger: 'Detecting danger signs...',
}
const TEXT_STAGE_META = {
  detect: 'Detecting visit type...',
  form: 'Extracting structured form...',
  danger: 'Detecting danger signs...',
}

function PipelineProgress({ stages }) {
  return (
    <div className="pipeline-progress">
      {stages.map((stage) => (
        <div className={`progress-step ${stage.status}`} key={stage.key}>
          <span className="step-icon">
            {stage.status === 'done' ? '\u2713' : stage.status === 'running' ? '\u25CF' : '\u25CB'}
          </span>
          <span className="step-label">
            {stage.label}
            {stage.status === 'done' && stage.time != null && <span className="step-time"> ({stage.time}s)</span>}
          </span>
        </div>
      ))}
    </div>
  )
}

function prettyLabel(text) {
  return String(text || '')
    .replaceAll('_', ' ')
    .replace(/\b\w/g, (c) => c.toUpperCase())
}

function keyValueRows(data, prefix = '') {
  if (!data || typeof data !== 'object' || Array.isArray(data)) return []
  const rows = []
  Object.entries(data).forEach(([key, value]) => {
    const fullKey = prefix ? `${prefix} > ${prettyLabel(key)}` : prettyLabel(key)
    if (value && typeof value === 'object' && !Array.isArray(value)) {
      rows.push(...keyValueRows(value, fullKey))
      return
    }
    if (Array.isArray(value)) {
      rows.push({
        key: fullKey,
        value: value.length ? value.map((v) => (typeof v === 'object' ? JSON.stringify(v) : String(v))).join(', ') : '—',
      })
      return
    }
    rows.push({ key: fullKey, value: value ?? '—' })
  })
  return rows
}

function App() {
  // Native APK: dark status-bar icons (LinkedIn-style) so the airplane icon
  // and clock render visibly against Sakhi's light content. No-op in browser.
  useEffect(() => {
    (async () => {
      try {
        await StatusBar.setOverlaysWebView({ overlay: true })
        await StatusBar.setStyle({ style: Style.Light })
      } catch {
        /* not running in a native Capacitor context */
      }
    })()
  }, [])

  const [activeTab, setActiveTab] = useState('voice')
  const [health, setHealth] = useState('Checking backend...')
  const [apiReachable, setApiReachable] = useState(null) // null = unknown, true/false after probe
  const [serverUrlInput, setServerUrlInput] = useState(API_BASE)
  const [serverUrlEditing, setServerUrlEditing] = useState(false)
  const [examples, setExamples] = useState([])
  const [history, setHistory] = useState(() => {
    try { return JSON.parse(localStorage.getItem('sakhi_history') || '[]') } catch { return [] }
  })
  const [viewingHistory, setViewingHistory] = useState(null)

  // Shared by Voice + Field record tabs (a single patient context per session).
  // Text tab and Field on-device card keep separate visit-type state below.
  const [recordingVisitType, setRecordingVisitType] = useState('auto')
  const [metadata, setMetadata] = useState(initialMetadata)
  const [textVisitType, setTextVisitType] = useState('auto')
  const [textInput, setTextInput] = useState('')
  const [selectedExample, setSelectedExample] = useState('')

  const [audioFile, setAudioFile] = useState(null)
  const [audioUrl, setAudioUrl] = useState('')
  const [isRecording, setIsRecording] = useState(false)

  // Curated audio examples served by the backend at /api/audio-examples.
  // Lets a judge play a real role-play clip + run extraction without
  // needing their own Hindi audio.
  const [audioExamples, setAudioExamples] = useState([])
  const [selectedAudioExample, setSelectedAudioExample] = useState('')

  // On-demand English translation of the Hindi transcript. Not in the main
  // extraction path — fires only when the reviewer clicks Translate.
  const [translation, setTranslation] = useState({ loading: false, english: '', error: '' })

  const mediaRecorderRef = useRef(null)
  const streamRef = useRef(null)
  const chunksRef = useRef([])

  const [voiceState, setVoiceState] = useState({
    loading: false,
    error: '',
    transcript: '',
    visitType: '',
    form: null,
    danger: null,
    timing: null,
    _raw: null,
  })

  const [textState, setTextState] = useState({
    loading: false,
    error: '',
    visitType: '',
    form: null,
    danger: null,
    timing: null,
    _raw: null,
  })

  const [pipelineStages, setPipelineStages] = useState([])

  // Field Mode state
  const [isOnline, setIsOnline] = useState(navigator.onLine)
  const [offlineQueue, setOfflineQueue] = useState([])
  const [fieldRecording, setFieldRecording] = useState(false)
  const [syncingId, setSyncingId] = useState(null)
  const fieldRecorderRef = useRef(null)
  const fieldStreamRef = useRef(null)
  const fieldSessionIdRef = useRef(null)
  const [fieldError, setFieldError] = useState('')
  const [playingId, setPlayingId] = useState(null)
  const playAudioRef = useRef(null)
  const [orphanedSessions, setOrphanedSessions] = useState([])

  // On-device Field text-in extraction (Cactus + pipeline.js)
  const [fieldOnDeviceText, setFieldOnDeviceText] = useState('')
  const [fieldOnDeviceVisitType, setFieldOnDeviceVisitType] = useState('auto')
  const [fieldOnDeviceState, setFieldOnDeviceState] = useState({
    loading: false, error: '', transcript: '', visitType: '', form: null, danger: null, timing: null, _raw: null,
  })
  const [devViewEnabled, setDevViewEnabled] = useState(false)

  // Cactus on-device probe state
  const [cactusStatus, setCactusStatus] = useState(null)
  const [cactusBusy, setCactusBusy] = useState(false)
  const [cactusLog, setCactusLog] = useState([])
  const [importProgress, setImportProgress] = useState(null) // { phase, pct, entries, totalEntries, bytes }
  const pushLog = (msg) => setCactusLog((prev) => [...prev.slice(-30), `[${new Date().toLocaleTimeString('en-IN')}] ${msg}`])

  useEffect(() => {
    fetch(`${API_BASE}/api/health`)
      .then((r) => r.json())
      .then((d) => {
        setHealth(
          d.whisper
            ? `API: ${d.status} · LLM: ${d.model} · ASR: ${d.whisper}`
            : `API: ${d.status} · Model: ${d.model}`
        )
        setApiReachable(true)
      })
      .catch(() => {
        setHealth(`API not reachable at ${API_BASE || window.location.origin}`)
        setApiReachable(false)
      })

    fetch(`${API_BASE}/api/examples`)
      .then((r) => r.json())
      .then((data) => {
        setExamples(data || [])
        const defaultEx = (data || []).find((e) => e.default) || data?.[0]
        if (defaultEx) {
          setSelectedExample(defaultEx.label)
          setTextInput(defaultEx.transcript || '')
        }
      })
      .catch(() => {})

    fetch(`${API_BASE}/api/audio-examples`)
      .then((r) => r.json())
      .then((data) => setAudioExamples(Array.isArray(data) ? data : []))
      .catch(() => {})
  }, [])

  async function loadAudioExample(id) {
    setSelectedAudioExample(id)
    if (!id) return
    const ex = audioExamples.find((e) => e.id === id)
    if (!ex) return
    setVoiceState((s) => ({ ...s, error: '' }))
    setTranslation({ loading: false, english: '', error: '' })
    try {
      const res = await fetch(`${API_BASE}${ex.url}`)
      if (!res.ok) throw new Error(`fetch ${ex.url} → ${res.status}`)
      const blob = await res.blob()
      const file = new File([blob], ex.file, { type: blob.type || 'audio/ogg' })
      if (audioUrl) URL.revokeObjectURL(audioUrl)
      setAudioFile(file)
      setAudioUrl(URL.createObjectURL(file))
      if (ex.visit_type_hint) setRecordingVisitType(ex.visit_type_hint)
    } catch (err) {
      setVoiceState((s) => ({ ...s, error: `Could not load example: ${err.message}` }))
    }
  }

  async function translateTranscript() {
    const text = (voiceState.transcript || '').trim()
    if (!text) return
    setTranslation({ loading: true, english: '', error: '' })
    try {
      const res = await fetch(`${API_BASE}/api/translate`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ text }),
      })
      if (!res.ok) throw new Error(`HTTP ${res.status}`)
      const data = await res.json()
      setTranslation({ loading: false, english: data.english || '', error: '' })
    } catch (err) {
      setTranslation({ loading: false, english: '', error: err.message })
    }
  }

  function saveServerUrl() {
    const cleaned = (serverUrlInput || '').trim().replace(/\/+$/, '')
    if (!cleaned) {
      try { localStorage.removeItem('sakhi_server_url') } catch (_) {}
    } else {
      try { localStorage.setItem('sakhi_server_url', cleaned) } catch (_) {}
    }
    // Reload so every module-level API_BASE caller picks up the new value.
    window.location.reload()
  }

  useEffect(() => {
    return () => {
      if (audioUrl) URL.revokeObjectURL(audioUrl)
      if (streamRef.current) {
        streamRef.current.getTracks().forEach((t) => t.stop())
      }
    }
  }, [audioUrl])

  useEffect(() => {
    if (metadata.asha_id) localStorage.setItem('sakhi_asha_id', metadata.asha_id)
  }, [metadata.asha_id])

  // Online/offline detection + queue loading.
  // On network flip, immediately reflect the new API reachability so the
  // top banner + Field Mode badge stop showing a stale "Connected" state.
  useEffect(() => {
    const probeHealth = () => {
      fetch(`${API_BASE}/api/health`)
        .then((r) => r.json())
        .then((d) => {
          setHealth(
            d.whisper
              ? `API: ${d.status} · LLM: ${d.model} · ASR: ${d.whisper}`
              : `API: ${d.status} · Model: ${d.model}`
          )
          setApiReachable(true)
        })
        .catch(() => {
          setHealth(`API not reachable at ${API_BASE || window.location.origin}`)
          setApiReachable(false)
        })
    }
    const goOnline = () => {
      setIsOnline(true)
      probeHealth()
    }
    const goOffline = () => {
      setIsOnline(false)
      setApiReachable(false)
      setHealth('API not reachable — phone is offline')
    }
    window.addEventListener('online', goOnline)
    window.addEventListener('offline', goOffline)
    loadQueue()
    return () => {
      window.removeEventListener('online', goOnline)
      window.removeEventListener('offline', goOffline)
    }
  }, [])

  async function loadQueue() {
    const q = await getQueue()
    setOfflineQueue(q)
  }

  async function loadOrphaned() {
    try {
      const list = await listOrphanedSessions()
      setOrphanedSessions(list)
    } catch {
      setOrphanedSessions([])
    }
  }

  useEffect(() => {
    if (activeTab === 'field') loadOrphaned()
  }, [activeTab])

  async function recoverOrphan(sessionId, visitType) {
    try {
      const result = await assembleChunks(sessionId)
      if (result && result.blob && result.blob.size > 0) {
        await saveRecording(
          result.blob,
          visitType || 'auto',
          `Recovered ${new Date().toLocaleTimeString('en-IN')}`,
          result.metadata,
        )
      }
      await clearChunks(sessionId)
      await loadOrphaned()
      await loadQueue()
    } catch (err) {
      setFieldError(`Recovery failed: ${err.message}`)
    }
  }

  async function discardOrphan(sessionId) {
    try {
      await clearChunks(sessionId)
      await loadOrphaned()
    } catch (err) {
      setFieldError(`Discard failed: ${err.message}`)
    }
  }

  async function cactusCheck() {
    setCactusBusy(true)
    try {
      const s = await Cactus.isAvailable()
      setCactusStatus(s)
      pushLog(`status: available=${s.available} modelPresent=${s.modelPresent ?? false}${s.modelFound ? ` @ ${s.modelFound}` : ''}`)
    } catch (err) {
      pushLog(`status check failed: ${err.message || err}`)
      setCactusStatus({ available: false, error: String(err) })
    } finally {
      setCactusBusy(false)
    }
  }

  async function cactusLoad() {
    setCactusBusy(true)
    try {
      pushLog('loading model...')
      const r = await Cactus.init()
      pushLog(`model loaded in ${r.initMs || '?'}ms from ${r.modelPath}`)
      setCactusStatus((s) => ({ ...(s || {}), ...r, loaded: true }))
    } catch (err) {
      pushLog(`init failed: ${err.message || err}`)
    } finally {
      setCactusBusy(false)
    }
  }

  async function cactusTest() {
    setCactusBusy(true)
    try {
      pushLog('running test completion...')
      const t0 = Date.now()
      const r = await Cactus.complete({
        messages: [
          { role: 'user', content: 'नमस्ते, आप कैसे हैं?' },
        ],
        options: { max_tokens: 64, temperature: 0.3 },
      })
      const elapsed = Date.now() - t0
      pushLog(`got ${r.text?.length || 0} chars in ${elapsed}ms (decode ${r.decodeTps?.toFixed?.(1) || '?'} tps)`)
      pushLog(`text: ${(r.text || r.raw || '').slice(0, 200)}`)
    } catch (err) {
      pushLog(`complete failed: ${err.message || err}`)
    } finally {
      setCactusBusy(false)
    }
  }

  async function cactusUnload() {
    setCactusBusy(true)
    try {
      await Cactus.destroy()
      pushLog('model unloaded')
      setCactusStatus((s) => ({ ...(s || {}), loaded: false, handle: 0 }))
    } catch (err) {
      pushLog(`destroy failed: ${err.message || err}`)
    } finally {
      setCactusBusy(false)
    }
  }

  async function cactusImport() {
    setCactusBusy(true)
    setImportProgress(null)
    try {
      pushLog('opening file picker...')
      // We log only on every 5% crossover (or on terminal events) to keep
      // the log card readable — the progress bar itself updates per 1%.
      let lastLogBucket = -1
      const r = await Cactus.importModelFromZip((evt) => {
        setImportProgress(evt)
        const mb = evt.bytes != null ? (evt.bytes / (1024 * 1024)).toFixed(0) : '?'
        if (evt.phase === 'scanning_done') {
          const totalMb = evt.totalBytes ? (evt.totalBytes / (1024 * 1024)).toFixed(0) : '?'
          pushLog(`starting extract (zip is ${totalMb} MB)`)
        } else if (evt.phase === 'extracting') {
          const bucket = Math.floor((evt.pct || 0) / 5)
          if (bucket > lastLogBucket) {
            lastLogBucket = bucket
            pushLog(`extract ${evt.pct}% — ${evt.entries} files, ${mb} MB`)
          }
        } else if (evt.phase === 'done') {
          pushLog(`extract 100% — ${evt.entries} files (${mb} MB) written`)
        }
      })
      if (r.cancelled) {
        pushLog('import cancelled')
        return
      }
      const mb = r.bytes ? (r.bytes / (1024 * 1024)).toFixed(0) : '?'
      pushLog(`imported ${r.entries} files (${mb} MB) → ${r.modelPath}`)
      // Re-probe so the UI sees the new model.
      const s = await Cactus.isAvailable()
      setCactusStatus(s)
    } catch (err) {
      pushLog(`import failed: ${err.message || err}`)
    } finally {
      setCactusBusy(false)
      setImportProgress(null)
    }
  }

  async function processFieldOnDevice() {
    const text = fieldOnDeviceText.trim()
    if (!text) {
      setFieldOnDeviceState((s) => ({ ...s, error: 'Type a Hindi note first.' }))
      return
    }
    setFieldOnDeviceState({ loading: true, error: '', transcript: '', visitType: '', form: null, danger: null, timing: null, _raw: null })
    try {
      const result = await runPipeline({
        engine: Cactus,
        transcript: text,
        visitType: fieldOnDeviceVisitType === 'auto' ? null : fieldOnDeviceVisitType,
        metadata,
      })
      setFieldOnDeviceState({
        loading: false,
        error: '',
        transcript: result.transcript,
        visitType: result.visitType,
        form: result.form,
        danger: result.danger,
        timing: result.timing,
        _raw: result._raw || null,
      })
      saveToHistory('field', result.visitType, result.form, result.danger, result.transcript, result.timing)
    } catch (err) {
      setFieldOnDeviceState((s) => ({ ...s, loading: false, error: `On-device extraction failed: ${err.message || err}` }))
    }
  }

  async function startFieldRecording() {
    setFieldError('')
    // Stop any active recorders first
    if (mediaRecorderRef.current && mediaRecorderRef.current.state !== 'inactive') {
      mediaRecorderRef.current.stop()
    }
    if (fieldRecorderRef.current && fieldRecorderRef.current.state !== 'inactive') {
      fieldRecorderRef.current.stop()
    }
    // Release all mic streams
    if (streamRef.current) {
      streamRef.current.getTracks().forEach((t) => t.stop())
      streamRef.current = null
    }
    if (fieldStreamRef.current) {
      fieldStreamRef.current.getTracks().forEach((t) => t.stop())
      fieldStreamRef.current = null
    }
    // Small delay to let the OS release the device
    await new Promise((r) => setTimeout(r, 300))
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: { echoCancellation: false, noiseSuppression: false, autoGainControl: true }
      })
      fieldStreamRef.current = stream
      const recorder = new MediaRecorder(stream)
      const sessionId = (crypto.randomUUID && crypto.randomUUID()) || `s-${Date.now()}-${Math.random().toString(36).slice(2)}`
      fieldSessionIdRef.current = sessionId
      const capturedVisitType = recordingVisitType
      const capturedMetadata = { ...metadata }
      recorder.ondataavailable = async (e) => {
        if (e.data && e.data.size > 0) {
          try { await appendChunk(sessionId, e.data, capturedVisitType, capturedMetadata) } catch (err) { console.error('appendChunk failed', err) }
        }
      }
      recorder.onstop = async () => {
        stream.getTracks().forEach((t) => t.stop())
        fieldStreamRef.current = null
        try {
          const result = await assembleChunks(sessionId)
          if (result && result.blob && result.blob.size > 0) {
            await saveRecording(result.blob, capturedVisitType, '', capturedMetadata)
          }
          await clearChunks(sessionId)
        } catch (err) {
          setFieldError(`Save failed: ${err.message}`)
        }
        fieldSessionIdRef.current = null
        await loadQueue()
        await loadOrphaned()
      }
      fieldRecorderRef.current = recorder
      recorder.start(5000)
      setFieldRecording(true)
    } catch (err) {
      setFieldError(`Microphone error: ${err.name}: ${err.message}`)
    }
  }

  function stopFieldRecording() {
    if (!fieldRecorderRef.current) return
    fieldRecorderRef.current.stop()
    setFieldRecording(false)
  }

  async function syncRecording(id) {
    setSyncingId(id)
    setPipelineStages([])
    setVoiceState({ loading: true, error: '', transcript: '', visitType: '', form: null, danger: null, timing: null })
    setActiveTab('voice')

    const entry = await getRecording(id)
    if (!entry) { setSyncingId(null); return }

    await updateRecordingStatus(id, 'processing')
    await loadQueue()

    const file = new File([entry.audioBlob], `field-${entry.id}.webm`, { type: entry.audioType })
    const formData = new FormData()
    formData.append('audio', file)
    formData.append('visit_type', entry.visitType)
    appendMetadataToFormData(formData, entry.metadata)

    try {
      const res = await fetch(`${API_BASE}/api/process-audio-stream`, { method: 'POST', body: formData })
      const reader = res.body.getReader()
      const decoder = new TextDecoder()
      let buffer = ''

      await new Promise((resolve, reject) => {
        function read() {
          reader.read().then(({ done, value }) => {
            if (done) { resolve(); return }
            buffer += decoder.decode(value, { stream: true })
            const lines = buffer.split('\n')
            buffer = lines.pop() || ''
            for (const line of lines) {
              if (!line.startsWith('data: ')) continue
              const evt = JSON.parse(line.slice(6))
              handleSSE(evt, 'voice', VOICE_STAGE_META)
            }
            read()
          }).catch(reject)
        }
        read()
      })

      await removeRecording(id)
      await loadQueue()
    } catch (err) {
      await updateRecordingStatus(id, 'pending')
      await loadQueue()
      setVoiceState((s) => ({ ...s, loading: false, error: `Sync failed: ${err.message}` }))
    }
    setSyncingId(null)
  }

  async function syncAll() {
    const pending = offlineQueue.filter((e) => e.status === 'pending')
    for (const entry of pending) {
      await syncRecording(entry.id)
    }
  }

  async function removeFromQueue(id) {
    await removeRecording(id)
    await loadQueue()
  }

  async function clearAllQueue() {
    await clearQueue()
    await loadQueue()
  }

  async function playRecording(id) {
    if (playingId === id) {
      if (playAudioRef.current) { playAudioRef.current.pause(); playAudioRef.current = null }
      setPlayingId(null)
      return
    }
    if (playAudioRef.current) { playAudioRef.current.pause(); playAudioRef.current = null }
    const entry = await getRecording(id)
    if (!entry) return
    const url = URL.createObjectURL(entry.audioBlob)
    const audio = new Audio(url)
    audio.onended = () => { URL.revokeObjectURL(url); setPlayingId(null); playAudioRef.current = null }
    playAudioRef.current = audio
    setPlayingId(id)
    audio.play()
  }

  const dangerSigns = useMemo(
    () => textState.danger?.danger_signs || voiceState.danger?.danger_signs || fieldOnDeviceState.danger?.danger_signs || [],
    [textState.danger, voiceState.danger, fieldOnDeviceState.danger]
  )

  async function startRecording() {
    // Release any existing mic streams first
    if (fieldStreamRef.current) {
      fieldStreamRef.current.getTracks().forEach((t) => t.stop())
      fieldStreamRef.current = null
    }
    if (streamRef.current) {
      streamRef.current.getTracks().forEach((t) => t.stop())
      streamRef.current = null
    }
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
      streamRef.current = stream
      const recorder = new MediaRecorder(stream)
      chunksRef.current = []
      recorder.ondataavailable = (e) => {
        if (e.data.size > 0) chunksRef.current.push(e.data)
      }
      recorder.onstop = () => {
        const blob = new Blob(chunksRef.current, { type: 'audio/webm' })
        const file = new File([blob], `recording-${Date.now()}.webm`, { type: 'audio/webm' })
        if (audioUrl) URL.revokeObjectURL(audioUrl)
        setAudioFile(file)
        setAudioUrl(URL.createObjectURL(blob))
        stream.getTracks().forEach((t) => t.stop())
        streamRef.current = null
      }
      mediaRecorderRef.current = recorder
      recorder.start(5000)
      setIsRecording(true)
    } catch {
      setVoiceState((s) => ({ ...s, error: 'Microphone permission denied or unavailable.' }))
    }
  }

  function stopRecording() {
    if (!mediaRecorderRef.current) return
    mediaRecorderRef.current.stop()
    setIsRecording(false)
  }

  function onUploadAudio(event) {
    const file = event.target.files?.[0]
    if (!file) return
    if (audioUrl) URL.revokeObjectURL(audioUrl)
    setAudioFile(file)
    setAudioUrl(URL.createObjectURL(file))
    setVoiceState((s) => ({ ...s, error: '' }))
  }

  function handleSSE(evt, source, stageMeta) {
    if (evt.error) {
      const setter = source === 'voice' ? setVoiceState : setTextState
      setter((s) => ({ ...s, loading: false, error: evt.error }))
      return
    }

    if (evt.stage === 'complete') {
      const setter = source === 'voice' ? setVoiceState : setTextState
      setter({
        loading: false,
        error: '',
        transcript: evt.transcript || '',
        visitType: evt.visit_type || '',
        form: evt.form || {},
        danger: evt.danger || {},
        timing: evt.timing || {},
        _raw: {
          tool_calls: evt.tool_calls || [],
          form: evt.form || {},
          danger: evt.danger || {},
          metadata: evt.metadata || null,
        },
      })
      setPipelineStages((prev) => prev.map((s) => ({ ...s, status: 'done' })))
      saveToHistory(source, evt.visit_type, evt.form, evt.danger, evt.transcript || null, evt.timing)
      return
    }

    if (evt.status === 'running') {
      const label = stageMeta[evt.stage] || evt.stage
      setPipelineStages((prev) => {
        const exists = prev.find((s) => s.key === evt.stage)
        if (exists) return prev.map((s) => s.key === evt.stage ? { ...s, status: 'running' } : s)
        return [...prev, { key: evt.stage, label, status: 'running', time: null }]
      })
    }

    if (evt.status === 'done') {
      setPipelineStages((prev) =>
        prev.map((s) => s.key === evt.stage ? { ...s, status: 'done', time: evt.time ?? null } : s)
      )
      if (evt.transcript) {
        setVoiceState((s) => ({ ...s, transcript: evt.transcript }))
      }
    }
  }

  function processVoice() {
    if (!audioFile) {
      setVoiceState((s) => ({ ...s, error: 'Upload or record audio first.' }))
      return
    }
    setVoiceState({ loading: true, error: '', transcript: '', visitType: '', form: null, danger: null, timing: null, _raw: null })
    setPipelineStages([])

    const formData = new FormData()
    formData.append('audio', audioFile)
    formData.append('visit_type', recordingVisitType)
    appendMetadataToFormData(formData, metadata)

    fetch(`${API_BASE}/api/process-audio-stream`, { method: 'POST', body: formData })
      .then((res) => {
        const reader = res.body.getReader()
        const decoder = new TextDecoder()
        let buffer = ''

        function read() {
          reader.read().then(({ done, value }) => {
            if (done) return
            buffer += decoder.decode(value, { stream: true })
            const lines = buffer.split('\n')
            buffer = lines.pop() || ''
            for (const line of lines) {
              if (!line.startsWith('data: ')) continue
              const evt = JSON.parse(line.slice(6))
              handleSSE(evt, 'voice', VOICE_STAGE_META)
            }
            read()
          })
        }
        read()
      })
      .catch((err) => {
        setVoiceState((s) => ({ ...s, loading: false, error: err.message }))
      })
  }

  function processText() {
    if (!textInput.trim()) {
      setTextState((s) => ({ ...s, error: 'Transcript is empty.' }))
      return
    }
    setTextState({ loading: true, error: '', visitType: '', form: null, danger: null, timing: null, _raw: null })
    setPipelineStages([])

    fetch(`${API_BASE}/api/process-text-stream`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ transcript: textInput, visit_type: textVisitType }),
    })
      .then((res) => {
        const reader = res.body.getReader()
        const decoder = new TextDecoder()
        let buffer = ''

        function read() {
          reader.read().then(({ done, value }) => {
            if (done) return
            buffer += decoder.decode(value, { stream: true })
            const lines = buffer.split('\n')
            buffer = lines.pop() || ''
            for (const line of lines) {
              if (!line.startsWith('data: ')) continue
              const evt = JSON.parse(line.slice(6))
              handleSSE(evt, 'text', TEXT_STAGE_META)
            }
            read()
          })
        }
        read()
      })
      .catch((err) => {
        setTextState((s) => ({ ...s, loading: false, error: err.message }))
      })
  }

  function onSelectExample(label) {
    setSelectedExample(label)
    const ex = examples.find((e) => e.label === label)
    if (ex) setTextInput(ex.transcript || '')
  }

  function downloadJSON() {
    const data = activeState.form
    if (!data) return
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `sakhi-${activeState.visitType || 'form'}-${Date.now()}.json`
    a.click()
    URL.revokeObjectURL(url)
  }

  function downloadCSV() {
    const rows = keyValueRows(activeState.form)
    if (!rows.length) return
    const csv = 'Field,Value\n' + rows.map((r) => `"${r.key}","${String(r.value).replace(/"/g, '""')}"`).join('\n')
    const blob = new Blob([csv], { type: 'text/csv' })
    const url = URL.createObjectURL(blob)
    const a = document.createElement('a')
    a.href = url
    a.download = `sakhi-${activeState.visitType || 'form'}-${Date.now()}.csv`
    a.click()
    URL.revokeObjectURL(url)
  }

  const saveToHistory = useCallback((source, visitType, form, danger, transcript, timing) => {
    const entry = {
      id: Date.now(),
      date: new Date().toLocaleString('en-IN'),
      source,
      visitType,
      form,
      danger,
      transcript: transcript || null,
      timing,
    }
    setHistory((prev) => {
      const updated = [entry, ...prev].slice(0, 50)
      localStorage.setItem('sakhi_history', JSON.stringify(updated))
      return updated
    })
  }, [])

  const activeState = activeTab === 'voice'
    ? voiceState
    : activeTab === 'field'
      ? fieldOnDeviceState
      : textState

  return (
    <div className="app-shell">
      <header className="hero">
        <h1>Sakhi (सखी)</h1>
        <p>AI companion for India&apos;s ASHA health workers</p>
        <div className="badge-row">
          <span className="badge">Gemma 4 E4B</span>
          <span className="badge">Offline-First</span>
          <span className="badge">Hindi Voice</span>
        </div>
      </header>

      <div className={`status-line ${apiReachable === false ? 'status-line-error' : ''}`}>
        {health}
        {' · '}
        <button
          type="button"
          className="link-button"
          onClick={() => setServerUrlEditing((v) => !v)}
        >
          {serverUrlEditing ? 'cancel' : 'change server'}
        </button>
      </div>
      {serverUrlEditing && (
        <div className="server-url-editor">
          <label>
            <span>Backend server URL</span>
            <input
              type="url"
              value={serverUrlInput}
              onChange={(e) => setServerUrlInput(e.target.value)}
              placeholder="http://192.168.1.9:8000"
              autoComplete="off"
              autoCapitalize="none"
              spellCheck={false}
            />
          </label>
          <div className="server-url-actions">
            <button className="btn primary" onClick={saveServerUrl}>Save &amp; reload</button>
            <button
              className="btn secondary"
              onClick={() => {
                setServerUrlInput(defaultApiBase())
              }}
            >
              Reset
            </button>
          </div>
          <p className="server-url-hint">
            On the phone APK, set this to <code>http://&lt;PC-LAN-IP&gt;:8000</code> (e.g. <code>http://192.168.1.9:8000</code>).
            Saved in this device's localStorage; survives reinstalls only if app data isn't cleared.
          </p>
        </div>
      )}

      <div className="tabs">
        <button className={activeTab === 'voice' ? 'active' : ''} onClick={() => setActiveTab('voice')}>
          Voice to Form
        </button>
        <button className={activeTab === 'text' ? 'active' : ''} onClick={() => setActiveTab('text')}>
          Text to Form
        </button>
        <button className={activeTab === 'field' ? 'active' : ''} onClick={() => setActiveTab('field')}>
          Field Mode {offlineQueue.length > 0 ? `(${offlineQueue.length})` : ''}
        </button>
        <button className={activeTab === 'about' ? 'active' : ''} onClick={() => setActiveTab('about')}>
          About &amp; Impact
        </button>
        {history.length > 0 && (
          <button className={activeTab === 'history' ? 'active' : ''} onClick={() => { setActiveTab('history'); setViewingHistory(null) }}>
            History ({history.length})
          </button>
        )}
      </div>

      {activeTab === 'voice' && (
        <section className="panel">
          <h2>Record or upload Hindi ASHA conversation</h2>
          <PatientMetadataHeader
            metadata={metadata}
            setMetadata={setMetadata}
            visitType={recordingVisitType}
            setVisitType={setRecordingVisitType}
          />
          {audioExamples.length > 0 && (
            <div className="card">
              <div className="text-tools">
                <select
                  value={selectedAudioExample}
                  onChange={(e) => loadAudioExample(e.target.value)}
                >
                  <option value="">Load voice example...</option>
                  {audioExamples.map((ex) => (
                    <option key={ex.id} value={ex.id}>
                      {ex.label}
                    </option>
                  ))}
                </select>
                <button
                  className="btn primary"
                  onClick={processVoice}
                  disabled={!audioFile || voiceState.loading}
                >
                  {voiceState.loading ? 'Processing...' : 'Extract from example'}
                </button>
              </div>
              {selectedAudioExample && (() => {
                const ex = audioExamples.find((e) => e.id === selectedAudioExample)
                return ex ? <p className="field-desc">{ex.description}</p> : null
              })()}
            </div>
          )}
          <div className="card">
            <div className="audio-tools audio-tools-3">
              <button className={`btn ${isRecording ? 'danger' : ''}`} onClick={isRecording ? stopRecording : startRecording}>
                {isRecording ? 'Stop Recording' : 'Start Recording'}
              </button>
              <label className="btn secondary">
                Upload Audio File
                <input type="file" accept="audio/*" onChange={onUploadAudio} hidden />
              </label>
              <button className="btn primary" onClick={processVoice} disabled={voiceState.loading}>
                {voiceState.loading ? 'Processing...' : 'Process Audio'}
              </button>
            </div>
            <audio className="audio-player" controls src={audioUrl || undefined} />
            {audioFile && <p className="file-name">{audioFile.name}</p>}
          </div>
          <div className="card">
            <h3>Transcript</h3>
            <pre className="transcript">{voiceState.transcript || 'Transcript will appear here after processing audio.'}</pre>
            {voiceState.transcript && (
              <div className="translate-row">
                <button
                  className="btn secondary"
                  onClick={translateTranscript}
                  disabled={translation.loading}
                >
                  {translation.loading ? 'Translating...' : 'Translate to English'}
                </button>
                {translation.error && (
                  <span className="error-text">{translation.error}</span>
                )}
              </div>
            )}
            {translation.english && (
              <>
                <h3 style={{ marginTop: 16 }}>English translation</h3>
                <pre className="transcript">{translation.english}</pre>
              </>
            )}
            <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 12, fontSize: 13, color: '#555', cursor: 'pointer' }}>
              <input
                type="checkbox"
                checked={devViewEnabled}
                onChange={(e) => setDevViewEnabled(e.target.checked)}
              />
              Developer view — show raw API response (tool_calls, parsed form, parsed danger)
            </label>
          </div>
        </section>
      )}

      {activeTab === 'text' && (
        <section className="panel">
          <h2>Paste transcript and extract structured form</h2>
          <div className="card">
            <div className="text-tools">
              <select value={selectedExample} onChange={(e) => onSelectExample(e.target.value)}>
                <option value="">Load example...</option>
                {examples.map((ex) => (
                  <option key={ex.label} value={ex.label}>
                    {ex.label}
                  </option>
                ))}
              </select>
              <select value={textVisitType} onChange={(e) => setTextVisitType(e.target.value)}>
                {VISIT_OPTIONS.map((opt) => (
                  <option key={opt.value} value={opt.value}>
                    {opt.label}
                  </option>
                ))}
              </select>
              <button className="btn primary" onClick={processText} disabled={textState.loading}>
                {textState.loading ? 'Extracting...' : 'Extract Structured Form'}
              </button>
            </div>
            <textarea
              className="text-input"
              value={textInput}
              onChange={(e) => setTextInput(e.target.value)}
              placeholder="Paste Hindi conversation transcript here..."
            />
            <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 12, fontSize: 13, color: '#555', cursor: 'pointer' }}>
              <input
                type="checkbox"
                checked={devViewEnabled}
                onChange={(e) => setDevViewEnabled(e.target.checked)}
              />
              Developer view — show raw API response (tool_calls, parsed form, parsed danger)
            </label>
          </div>
        </section>
      )}

      {activeTab === 'field' && (
        <section className="panel">
          <h2>Field Mode — Record Now, Process Later</h2>
          <PatientMetadataHeader
            metadata={metadata}
            setMetadata={setMetadata}
            visitType={recordingVisitType}
            setVisitType={setRecordingVisitType}
          />
          {orphanedSessions.length > 0 && (
            <div className="card" style={{ borderLeft: '4px solid #d97706', background: '#fffbeb' }}>
              <h3 style={{ marginTop: 0 }}>Unfinished recordings detected</h3>
              <p style={{ marginTop: 4 }}>
                {orphanedSessions.length} recording{orphanedSessions.length > 1 ? 's were' : ' was'} interrupted
                (tab closed, browser crashed, or phone locked). You can recover the partial audio or discard it.
              </p>
              <div className="queue-list">
                {orphanedSessions.map((o) => (
                  <div className="queue-item" key={o.sessionId}>
                    <div className="queue-meta">
                      <strong>{prettyLabel(o.visitType || 'auto')}</strong>
                      <span>{new Date(o.firstSeen).toLocaleString('en-IN')}</span>
                      <span>{o.chunkCount} chunk{o.chunkCount > 1 ? 's' : ''}</span>
                      <span>{(o.totalSize / 1024).toFixed(0)} KB</span>
                    </div>
                    <div className="queue-item-actions">
                      <button className="btn primary" onClick={() => recoverOrphan(o.sessionId, o.visitType)}>
                        Recover
                      </button>
                      <button className="btn secondary" onClick={() => discardOrphan(o.sessionId)}>
                        Discard
                      </button>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          )}
          <div className="card">
            <div className={`connectivity-badge ${apiReachable === true ? 'online' : 'offline'}`}>
              {apiReachable === true
                ? 'Connected — ready to sync'
                : isOnline
                  ? 'Workstation unreachable — recordings saved locally'
                  : 'Offline — recordings saved locally'}
            </div>
            <p className="field-desc">
              Record ASHA conversations during home visits. Audio is saved on your device
              and processed when you return to the health center.
            </p>
            <div className="audio-tools audio-tools-1">
              <button
                className={`btn ${fieldRecording ? 'danger' : 'primary'}`}
                onClick={fieldRecording ? stopFieldRecording : startFieldRecording}
              >
                {fieldRecording ? 'Stop & Save' : 'Record Visit'}
              </button>
            </div>
            {fieldError && <div className="error-banner">{fieldError}</div>}
          </div>

          <div className="card" style={{ borderLeft: '4px solid #0f766e' }}>
            <h3 style={{ marginTop: 0 }}>On-device text → form (no network)</h3>
            <p className="field-desc">
              Type a short Hindi note below and Gemma 4 E2B runs entirely on this phone via Cactus
              to extract the structured form + danger signs. Use this when you need instant feedback
              without laptop access. Voice recordings above sync to a health-center laptop for
              full-accuracy Whisper-Large processing.
            </p>
            <div className="text-tools">
              <select value={fieldOnDeviceVisitType} onChange={(e) => setFieldOnDeviceVisitType(e.target.value)}>
                {VISIT_OPTIONS.map((opt) => (
                  <option key={opt.value} value={opt.value}>{opt.label}</option>
                ))}
              </select>
              <button
                className="btn secondary"
                type="button"
                onClick={() => {
                  const anc = examples.find((e) => (e.label || '').toLowerCase().includes('preeclampsia'))
                    || examples.find((e) => e.visit_type === 'anc')
                  if (anc && anc.transcript) {
                    setFieldOnDeviceText(anc.transcript)
                    setFieldOnDeviceVisitType('anc')
                  }
                }}
                disabled={fieldOnDeviceState.loading || examples.length === 0}
              >
                Load ANC example
              </button>
              <button
                className="btn primary"
                onClick={processFieldOnDevice}
                disabled={fieldOnDeviceState.loading || !fieldOnDeviceText.trim()}
              >
                {fieldOnDeviceState.loading ? 'Processing on device...' : 'Process on device'}
              </button>
            </div>
            <textarea
              className="text-input"
              value={fieldOnDeviceText}
              onChange={(e) => setFieldOnDeviceText(e.target.value)}
              placeholder="मरीज़ का नाम सुनीता है, 24 साल, गर्भावस्था 32 सप्ताह, रक्तचाप 120/80..."
            />
            {fieldOnDeviceState.loading && (
              <p style={{ fontSize: 13, color: '#555', marginTop: 8 }}>
                First run loads the model (~10 s). On-device extraction typically takes 3–5 min
                total on this phone (form + danger signs).
              </p>
            )}
            <label style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 12, fontSize: 13, color: '#555', cursor: 'pointer' }}>
              <input
                type="checkbox"
                checked={devViewEnabled}
                onChange={(e) => setDevViewEnabled(e.target.checked)}
              />
              Developer view — show raw model output per stage
            </label>
          </div>

          {devViewEnabled && fieldOnDeviceState._raw && (
            <div className="card" style={{ background: '#0f172a', color: '#e2e8f0', fontFamily: 'ui-monospace, Menlo, Consolas, monospace' }}>
              <h3 style={{ marginTop: 0, color: '#93c5fd' }}>Raw model output</h3>
              {fieldOnDeviceState._raw.formError && (
                <p style={{ color: '#fca5a5', fontSize: 12, margin: '4px 0' }}>
                  form parse: {fieldOnDeviceState._raw.formError}
                </p>
              )}
              <div style={{ marginBottom: 12 }}>
                <div style={{ color: '#93c5fd', fontSize: 12, marginBottom: 4 }}>$ form extractor →</div>
                <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, background: '#020617', padding: 10, borderRadius: 4, maxHeight: 300, overflow: 'auto' }}>
{fieldOnDeviceState._raw.form || '(empty)'}
                </pre>
              </div>
              {fieldOnDeviceState._raw.dangerError && (
                <p style={{ color: '#fca5a5', fontSize: 12, margin: '4px 0' }}>
                  danger parse: {fieldOnDeviceState._raw.dangerError}
                </p>
              )}
              <div>
                <div style={{ color: '#93c5fd', fontSize: 12, marginBottom: 4 }}>$ danger extractor →</div>
                <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, background: '#020617', padding: 10, borderRadius: 4, maxHeight: 300, overflow: 'auto' }}>
{fieldOnDeviceState._raw.danger || '(empty)'}
                </pre>
              </div>
              {fieldOnDeviceState.timing && (
                <div style={{ marginTop: 12, fontSize: 12, color: '#94a3b8' }}>
                  {Object.entries(fieldOnDeviceState.timing).map(([k, v]) => {
                    const isMs = k.endsWith('_ms')
                    const label = isMs ? k.slice(0, -3) : k
                    const display = isMs
                      ? (Number(v) >= 1000 ? `${(Number(v) / 1000).toFixed(2)}s` : `${v}ms`)
                      : `${v}s`
                    return <span key={k} style={{ marginRight: 12 }}>{label}={display}</span>
                  })}
                </div>
              )}
            </div>
          )}

          {offlineQueue.length > 0 && (
            <div className="card">
              <div className="queue-header">
                <h3>Saved Recordings ({offlineQueue.length})</h3>
                <div className="queue-actions">
                  {apiReachable === true && (
                    <button className="btn primary" onClick={syncAll} disabled={syncingId != null}>
                      Sync All
                    </button>
                  )}
                  <button className="btn secondary" onClick={clearAllQueue}>Clear All</button>
                </div>
              </div>
              <div className="queue-list">
                {offlineQueue.map((entry) => (
                  <div className={`queue-item ${entry.status}`} key={entry.id}>
                    <div className="queue-meta">
                      <strong>{entry.label}</strong>
                      <span>{entry.date}</span>
                      <span>{prettyLabel(entry.visitType)}</span>
                      <span>{(entry.size / 1024).toFixed(0)} KB</span>
                      <span className={`queue-status ${entry.status}`}>
                        {entry.status === 'pending' ? 'Pending' : entry.status === 'processing' ? 'Processing...' : entry.status}
                      </span>
                    </div>
                    <div className="queue-item-actions">
                      <button className="btn secondary" onClick={() => playRecording(entry.id)}>
                        {playingId === entry.id ? 'Stop' : 'Play'}
                      </button>
                      {apiReachable === true && entry.status === 'pending' && (
                        <button className="btn secondary" onClick={() => syncRecording(entry.id)} disabled={syncingId != null}>
                          Sync
                        </button>
                      )}
                      <button className="btn secondary" onClick={() => removeFromQueue(entry.id)}>Remove</button>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          )}

          {offlineQueue.length === 0 && (
            <div className="card">
              <p>No recordings saved. Record a visit above — it will be stored on your device for later processing.</p>
            </div>
          )}

          <div className="card" style={{ borderLeft: '4px solid #6366f1', background: '#f5f3ff' }}>
            <h3 style={{ marginTop: 0 }}>On-Device Probe (Cactus)</h3>
            <p style={{ marginTop: 4, color: '#555' }}>
              Diagnostic for Cactus SDK + Gemma on-device inference. Push a Cactus-format model folder to
              <code> /sdcard/Download/</code> on the phone (must contain <code>config.txt</code>).
            </p>
            <div className="audio-tools">
              <button className="btn secondary" onClick={cactusCheck} disabled={cactusBusy}>Check Status</button>
              <button className="btn secondary" onClick={cactusImport} disabled={cactusBusy}>Import model (.zip)</button>
              <button className="btn secondary" onClick={cactusLoad} disabled={cactusBusy || !cactusStatus?.modelPresent}>Load Model</button>
              <button className="btn primary" onClick={cactusTest} disabled={cactusBusy || !cactusStatus?.loaded}>Test Hindi</button>
              <button className="btn secondary" onClick={cactusUnload} disabled={cactusBusy || !cactusStatus?.loaded}>Unload</button>
            </div>
            <p style={{ fontSize: 12, color: '#6b7280', marginTop: 6 }}>
              First-time setup: download the Gemma 4 E2B zip (~4.4 GB) to the
              phone's Downloads folder (USB transfer or Drive download — not
              WhatsApp; 2 GB cap), tap <strong>Import model</strong>, pick the
              zip, wait for extraction (~5 min), then <strong>Load Model</strong>.
            </p>
            {importProgress && (
              <div className="import-progress">
                <div className="import-progress-label">
                  {importProgress.phase === 'scanning_done' && `Scanned ${importProgress.totalEntries} entries — extracting...`}
                  {importProgress.phase === 'extracting' && (
                    <>
                      Extracting {importProgress.pct}% · {importProgress.entries}/{importProgress.totalEntries} files · {(importProgress.bytes / (1024 * 1024)).toFixed(0)} MB
                    </>
                  )}
                  {importProgress.phase === 'done' && `Extracted ${importProgress.entries} files — ${(importProgress.bytes / (1024 * 1024)).toFixed(0)} MB`}
                </div>
                <progress
                  className="import-progress-bar"
                  value={importProgress.pct || 0}
                  max={100}
                />
              </div>
            )}
            {cactusStatus && (
              <div style={{ fontSize: 13, color: '#374151', marginTop: 8 }}>
                <strong>Status:</strong> available={String(cactusStatus.available)} ·
                modelPresent={String(cactusStatus.modelPresent ?? false)} ·
                loaded={String(!!cactusStatus.loaded)} ·
                handle={cactusStatus.handle || 0}
                {cactusStatus.modelFound && <div style={{ fontSize: 11, color: '#555', wordBreak: 'break-all' }}>found: {cactusStatus.modelFound}</div>}
              </div>
            )}
            {cactusLog.length > 0 && (
              <pre style={{ fontSize: 12, background: '#fff', border: '1px solid #e5e7eb', padding: 8, marginTop: 8, maxHeight: 200, overflow: 'auto', whiteSpace: 'pre-wrap' }}>
                {cactusLog.join('\n')}
              </pre>
            )}
          </div>
        </section>
      )}

      {activeTab === 'about' && (
        <section className="panel">
          <div className="card about-card">
            <h2>What is Sakhi?</h2>
            <p>
              Sakhi (सखी — &quot;companion&quot;) is an AI-powered tool that converts Hindi voice conversations between
              ASHA health workers and patients into structured medical forms — instantly, offline, on a single laptop.
            </p>

            <h3>The Problem</h3>
            <p>
              India&apos;s 1 million+ ASHA workers conduct home visits for antenatal care, postnatal care, deliveries,
              and child health. After each visit, they manually fill paper forms — a process that takes 15-20 minutes,
              is error-prone, and often delayed. Many ASHA workers have limited literacy, making form-filling the
              hardest part of their job.
            </p>

            <h3>How Sakhi Works</h3>
            <div className="pipeline-steps">
              <div className="step">
                <strong>1. Hindi Voice Input</strong>
                <span>Record or upload the ASHA-patient conversation in Hindi/Hinglish</span>
              </div>
              <div className="step">
                <strong>2. Speech Recognition</strong>
                <span>Whisper Large V2 (Hindi-specialized, 3000hrs training) transcribes with 95% accuracy</span>
              </div>
              <div className="step">
                <strong>3. Number Normalization</strong>
                <span>Custom algorithm converts Hindi number words (एक सो दस = 110) to digits</span>
              </div>
              <div className="step">
                <strong>4. Structured Extraction</strong>
                <span>Gemma 4 E4B extracts vitals, patient info, and clinical data into NHM-standard forms</span>
              </div>
              <div className="step">
                <strong>5. Danger Sign Detection</strong>
                <span>Flags life-threatening conditions with evidence quotes — zero false alarms on normal visits</span>
              </div>
            </div>

            <h3>Why It Matters</h3>
            <ul>
              <li><strong>15-20 min saved per visit</strong> — ASHA workers do 5-10 visits/day, that&apos;s 1-3 hours saved daily</li>
              <li><strong>Offline-first</strong> — runs on a laptop with no internet, critical for rural India where 60% of visits happen</li>
              <li><strong>Hindi-native</strong> — first tool to handle Hindi medical speech with code-switching (Hindi + English medical terms)</li>
              <li><strong>Anti-hallucination</strong> — strict null policy for unmentioned fields, evidence-based danger signs only</li>
              <li><strong>22-second pipeline</strong> — voice to completed form in under 25 seconds</li>
            </ul>

            <h3>Technology</h3>
            <div className="tech-grid">
              <div className="tech-item">
                <strong>LLM</strong>
                <span>Google Gemma 4 E4B (8B params, Q4_K_M quantized, 5GB)</span>
              </div>
              <div className="tech-item">
                <strong>ASR</strong>
                <span>Collabora Whisper Large V2 Hindi (CTranslate2, 6GB)</span>
              </div>
              <div className="tech-item">
                <strong>Inference</strong>
                <span>Ollama with JSON mode — 146 tok/s on consumer GPU</span>
              </div>
              <div className="tech-item">
                <strong>Frontend</strong>
                <span>React + Vite (PWA-ready)</span>
              </div>
              <div className="tech-item">
                <strong>Backend</strong>
                <span>FastAPI (Python)</span>
              </div>
              <div className="tech-item">
                <strong>GPU</strong>
                <span>Runs on any 16GB+ VRAM GPU (tested: RTX 5070 Ti)</span>
              </div>
            </div>
          </div>
        </section>
      )}

      {activeTab === 'history' && (
        <section className="panel">
          {viewingHistory ? (
            <div className="card">
              <div className="history-detail-header">
                <button className="btn secondary" onClick={() => setViewingHistory(null)}>&larr; Back</button>
                <h3>{prettyLabel(viewingHistory.visitType)} — {viewingHistory.date}</h3>
              </div>
              {viewingHistory.transcript && (
                <div style={{ marginBottom: 12 }}>
                  <h4 style={{ color: '#0f766e', marginBottom: 4 }}>Transcript</h4>
                  <pre className="transcript">{viewingHistory.transcript}</pre>
                </div>
              )}
              <div className="results-grid">
                <div>
                  <h4 style={{ color: '#0f766e', marginBottom: 8 }}>Form Data</h4>
                  <div className="kv-grid">
                    {keyValueRows(viewingHistory.form).map((row) => (
                      <div className="kv-row" key={`${row.key}-${row.value}`}>
                        <span>{row.key}</span>
                        <strong>{String(row.value)}</strong>
                      </div>
                    ))}
                  </div>
                </div>
                <div>
                  <h4 style={{ color: '#0f766e', marginBottom: 8 }}>Danger Signs</h4>
                  <p className="referral">{prettyLabel(viewingHistory.danger?.referral_decision?.decision || 'No referral')}</p>
                  {(viewingHistory.danger?.danger_signs || []).map((item, idx) => (
                    <div className="danger-item" key={`${item.sign}-${idx}`}>
                      <strong>{item.sign}</strong>
                      <span>{prettyLabel(item.category)}</span>
                    </div>
                  ))}
                  {!(viewingHistory.danger?.danger_signs || []).length && <p>No danger signs detected.</p>}
                </div>
              </div>
            </div>
          ) : (
            <>
              <div className="history-header">
                <h2>Visit History</h2>
                <button className="btn secondary" onClick={() => { setHistory([]); localStorage.removeItem('sakhi_history') }}>Clear All</button>
              </div>
              <div className="history-list">
                {history.map((entry) => (
                  <div className="card history-entry" key={entry.id} onClick={() => setViewingHistory(entry)}>
                    <div className="history-meta">
                      <strong>{prettyLabel(entry.visitType)}</strong>
                      <span>{entry.source === 'voice' ? 'Voice' : entry.source === 'field' ? 'On-device' : 'Text'}</span>
                      <span>{entry.date}</span>
                      {entry.timing?.total_s && <span>{entry.timing.total_s}s</span>}
                    </div>
                    {entry.transcript && <p className="history-preview">{entry.transcript.slice(0, 100)}...</p>}
                  </div>
                ))}
              </div>
            </>
          )}
        </section>
      )}

      {activeState.error && <div className="error-banner">{activeState.error}</div>}

      {activeState.loading && pipelineStages.length > 0 && (
        <div className="card">
          <h3>Processing Pipeline</h3>
          <PipelineProgress stages={pipelineStages} />
        </div>
      )}

      {(activeState.form || activeState.danger) && (
        <section className="results-grid">
          <div className="card">
            <h3>
              Form Extraction {activeState.visitType ? <span className="muted">({prettyLabel(activeState.visitType)})</span> : null}
            </h3>
            {activeState.loading ? (
              <div className="loader">Running extraction pipeline...</div>
            ) : (
              <>
                <div className="kv-grid">
                  {keyValueRows(activeState.form).map((row) => (
                    <div className="kv-row" key={`${row.key}-${row.value}`}>
                      <span>{row.key}</span>
                      <strong>{String(row.value)}</strong>
                    </div>
                  ))}
                </div>
                <div className="export-buttons">
                  <button className="btn secondary" onClick={downloadJSON}>Export JSON</button>
                  <button className="btn secondary" onClick={downloadCSV}>Export CSV</button>
                </div>
              </>
            )}
          </div>

          <div className="card danger">
            <h3>Danger Signs & Referral</h3>
            {activeState.loading ? (
              <div className="loader">Analyzing danger signs...</div>
            ) : (
              <>
                <p className="referral">{prettyLabel(activeState.danger?.referral_decision?.decision || 'No referral decision')}</p>
                <p className="reason">{activeState.danger?.referral_decision?.reason || 'No reason provided.'}</p>
                <div className="danger-list">
                  {(activeState.danger?.danger_signs || []).map((item, idx) => (
                    <div className="danger-item" key={`${item.sign}-${idx}`}>
                      <strong>{item.sign}</strong>
                      <span>{prettyLabel(item.category)}</span>
                      {item.clinical_value ? <em>Value: {String(item.clinical_value)}</em> : null}
                      {item.utterance_evidence ? <p>&quot;{item.utterance_evidence}&quot;</p> : null}
                    </div>
                  ))}
                  {!dangerSigns.length && <p>No danger signs detected.</p>}
                </div>
              </>
            )}
          </div>
        </section>
      )}

      {activeState.timing && (
        <div className="timing">
          {Object.entries(activeState.timing).map(([k, v]) => {
            const isMs = k.endsWith('_ms')
            const label = prettyLabel(isMs ? k.slice(0, -3) : k)
            const display = isMs
              ? (Number(v) >= 1000 ? `${(Number(v) / 1000).toFixed(1)}s` : `${v}ms`)
              : `${v}s`
            return (
              <span key={k}>
                {label}: <strong>{display}</strong>
              </span>
            )
          })}
        </div>
      )}

      {devViewEnabled && activeTab !== 'field' && activeState._raw && (
        <div className="card" style={{ background: '#0f172a', color: '#e2e8f0', fontFamily: 'ui-monospace, Menlo, Consolas, monospace' }}>
          <h3 style={{ marginTop: 0, color: '#93c5fd' }}>Raw API response (workstation pipeline)</h3>
          {Array.isArray(activeState._raw.tool_calls) && activeState._raw.tool_calls.length > 0 && (
            <div style={{ marginBottom: 12 }}>
              <div style={{ color: '#93c5fd', fontSize: 12, marginBottom: 4 }}>$ tool_calls (Ollama function calling) →</div>
              <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, background: '#020617', padding: 10, borderRadius: 4, maxHeight: 300, overflow: 'auto' }}>
{JSON.stringify(activeState._raw.tool_calls, null, 2)}
              </pre>
            </div>
          )}
          <div style={{ marginBottom: 12 }}>
            <div style={{ color: '#93c5fd', fontSize: 12, marginBottom: 4 }}>$ form →</div>
            <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, background: '#020617', padding: 10, borderRadius: 4, maxHeight: 300, overflow: 'auto' }}>
{JSON.stringify(activeState._raw.form, null, 2)}
            </pre>
          </div>
          <div style={{ marginBottom: 12 }}>
            <div style={{ color: '#93c5fd', fontSize: 12, marginBottom: 4 }}>$ danger →</div>
            <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, background: '#020617', padding: 10, borderRadius: 4, maxHeight: 300, overflow: 'auto' }}>
{JSON.stringify(activeState._raw.danger, null, 2)}
            </pre>
          </div>
          {activeState._raw.metadata && (
            <div>
              <div style={{ color: '#93c5fd', fontSize: 12, marginBottom: 4 }}>$ metadata (header from ASHA) →</div>
              <pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-word', fontSize: 12, background: '#020617', padding: 10, borderRadius: 4, maxHeight: 200, overflow: 'auto' }}>
{JSON.stringify(activeState._raw.metadata, null, 2)}
              </pre>
            </div>
          )}
        </div>
      )}
    </div>
  )
}

export default App
