import { useState, useEffect, useRef } from 'react'
import { api } from '../api'
import { format as formatSql } from 'sql-formatter'

function prettySql(sql) {
  try {
    return formatSql(sql, { language: 'postgresql', tabWidth: 4, keywordCase: 'upper' })
  } catch {
    return sql
  }
}

const FIELD_TYPES = ['text', 'numeric', 'date']

// ── Calibrate modal ────────────────────────────────────────────────────────────

function fmt(n) {
  return Number(n).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
}

function CalibrateModal({ stack, sourceName, currentOffset, onClose, onApply }) {
  const [asOf, setAsOf] = useState('')
  const [known, setKnown] = useState('')
  const [computed, setComputed] = useState(null)  // raw sum from DB (no offset)
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState('')
  const [applyOffset, setApplyOffset] = useState('')
  const debounceRef = useRef(null)

  const knownNum = parseFloat(known)
  const hasKnown = known !== '' && !isNaN(knownNum)
  const plug = hasKnown && computed !== null ? knownNum - computed : null

  // Auto-fetch computed sum on mount (all transactions) and whenever date changes
  useEffect(() => {
    clearTimeout(debounceRef.current)
    debounceRef.current = setTimeout(async () => {
      setLoading(true); setError('')
      try {
        const r = await api.calibrateBalance(stack.name, sourceName, { as_of_date: asOf || null, known_balance: 0 })
        if (r.success) setComputed(Number(r.computed_sum))
        else setError(r.error)
      } catch (e) { setError(e.message) }
      finally { setLoading(false) }
    }, asOf ? 400 : 0)
    return () => clearTimeout(debounceRef.current)
  }, [asOf])

  // Keep applyOffset in sync with plug
  useEffect(() => {
    if (plug !== null) setApplyOffset(plug.toFixed(2))
  }, [plug])

  return (
    <div className="fixed inset-0 bg-black/40 flex items-center justify-center z-50" onMouseDown={e => { if (e.target === e.currentTarget) onClose() }}>
      <div className="bg-white rounded-lg shadow-xl w-[420px] p-5" onClick={e => e.stopPropagation()}>
        <div className="flex items-center justify-between mb-4">
          <span className="text-sm font-semibold text-gray-700">Calibrate — {sourceName}</span>
          <button onClick={onClose} className="text-gray-400 hover:text-gray-600">✕</button>
        </div>

        {/* Date */}
        <div className="mb-4">
          <label className="text-xs text-gray-500 block mb-1">As-of date</label>
          <input type="date" className="w-full border border-gray-200 rounded px-3 py-1.5 text-sm focus:outline-none focus:border-blue-400"
            value={asOf} onChange={e => setAsOf(e.target.value)} />
        </div>

        {/* Reconciliation table */}
        <div className="bg-gray-50 rounded border border-gray-200 mb-4 text-sm">
          <div className="flex items-center justify-between px-3 py-2 border-b border-gray-200">
            <span className="text-gray-500 text-xs">Data sum at date</span>
            <span className="font-mono text-gray-700">
              {loading ? <span className="text-gray-300">…</span> : computed !== null ? fmt(computed) : <span className="text-gray-300">—</span>}
            </span>
          </div>
          <div className="flex items-center justify-between px-3 py-2 border-b border-gray-200">
            <span className="text-gray-500 text-xs">Known balance</span>
            <input
              type="number" step="0.01"
              className="font-mono text-right bg-transparent border-0 focus:outline-none w-36 text-sm text-gray-700 placeholder-gray-300"
              placeholder="enter balance"
              value={known} onChange={e => setKnown(e.target.value)}
            />
          </div>
          <div className="flex items-center justify-between px-3 py-2 border-b border-gray-200">
            <span className="text-gray-500 text-xs">Current offset</span>
            <span className="font-mono text-gray-400">{fmt(currentOffset ?? 0)}</span>
          </div>
          <div className="flex items-center justify-between px-3 py-2 font-medium">
            <span className="text-gray-700 text-xs">Plug (offset needed)</span>
            <span className={`font-mono ${plug !== null ? 'text-blue-700' : 'text-gray-300'}`}>
              {plug !== null ? fmt(plug) : '—'}
            </span>
          </div>
        </div>

        {error && <p className="text-xs text-red-500 mb-3">{error}</p>}

        {/* Apply */}
        <div className="flex gap-2 items-center">
          <input type="number" step="0.01"
            className="flex-1 border border-gray-200 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:border-blue-400"
            placeholder="offset to apply"
            value={applyOffset} onChange={e => setApplyOffset(e.target.value)} />
          <button onClick={() => onApply(parseFloat(applyOffset))} disabled={applyOffset === '' || isNaN(parseFloat(applyOffset))}
            className="text-sm bg-green-600 text-white px-4 py-1.5 rounded hover:bg-green-700 disabled:opacity-40">
            Apply
          </button>
        </div>
      </div>
    </div>
  )
}

// ── Stack panel ────────────────────────────────────────────────────────────────

function StackPanel({ stack, sources, onUpdated, onStale, onViewGenerated, onSqlGenerated }) {
  const members = stack.sources || []

  const [label, setLabel] = useState(stack.label || '')
  const [fields, setFields] = useState(stack.fields || [])
  const [newField, setNewField] = useState({ name: '', type: 'text' })
  const [addingSrc, setAddingSrc] = useState('')

  // Per-source config: sign, offset, amount_field, date_field, field_map
  const [srcCfg, setSrcCfg] = useState(() =>
    Object.fromEntries(members.map(m => [m.source_name, {
      sign:         m.amount_sign ?? 1,
      offset:       m.balance_offset ?? 0,
      amount_field: m.amount_field || '',
      date_field:   m.date_field || '',
      field_map:    { ...(m.field_map || {}) },
    }]))
  )

  // Available columns from each source's dfv view
  const [srcFields, setSrcFields] = useState({})

  // Drag-to-reorder state
  const [dragIdx, setDragIdx] = useState(null)
  const [dragOverIdx, setDragOverIdx] = useState(null)
  const [srcDragIdx, setSrcDragIdx] = useState(null)
  const [srcDragOverIdx, setSrcDragOverIdx] = useState(null)

  // Calibrate
  const [calibratingSource, setCalibratingSource] = useState(null)

  // View / balance
  const [viewResult, setViewResult] = useState(null)
  const [netBalance, setNetBalance] = useState(null)
  const [balanceError, setBalanceError] = useState('')

  const [saving, setSaving] = useState(false)
  const [mappingsDirty, setMappingsDirty] = useState(false)
  const [error, setError] = useState('')

  // Fetch source columns whenever members change
  useEffect(() => {
    members.forEach(m => {
      api.getFields(m.source_name)
        .then(f => setSrcFields(prev => ({ ...prev, [m.source_name]: f.map(x => x.key) })))
        .catch(() => {})
    })
  }, [members.map(m => m.source_name).join(',')])

  // Live SQL preview — debounced; syncs current UI state to DB first so preview is accurate
  const previewTimer = useRef(null)
  useEffect(() => {
    clearTimeout(previewTimer.current)
    previewTimer.current = setTimeout(async () => {
      try {
        for (const m of members) {
          const cfg = srcCfg[m.source_name] || {}
          await api.upsertStackSource(stack.name, m.source_name, {
            field_map:      cfg.field_map    || {},
            amount_sign:    cfg.sign         ?? 1,
            balance_offset: cfg.offset       ?? 0,
            amount_field:   cfg.amount_field || null,
            date_field:     cfg.date_field   || null,
          })
        }
        await api.updateStack(stack.name, {
          fields,
          amount_field: amountCanonical || null,
          date_field:   dateCanonical   || null,
        })
        const r = await api.previewStackSql(stack.name)
        if (r.success) onSqlGenerated?.(r.sql)
      } catch {}
    }, 600)
    return () => clearTimeout(previewTimer.current)
  }, [
    JSON.stringify(fields),
    JSON.stringify(srcCfg),
    members.map(m => m.source_name).join(','),
  ])

  // Auto-detect canonical amount/date field from field types
  const amountCanonical = fields.find(f => f.type === 'numeric')?.name || stack.amount_field
  const dateCanonical   = fields.find(f => f.type === 'date')?.name   || stack.date_field

  // ── Label ──
  async function saveLabel() {
    setSaving(true); setError('')
    try { await api.updateStack(stack.name, { label }); onUpdated() }
    catch (e) { setError(e.message) }
    finally { setSaving(false) }
  }

  // ── Fields ──
  async function addField() {
    if (!newField.name) return
    const updated = [...fields, { name: newField.name, type: newField.type }]
    setFields(updated)
    setNewField({ name: '', type: 'text' })
    await api.updateStack(stack.name, { fields: updated })
    onUpdated()
  }

  async function removeField(name) {
    const updated = fields.filter(f => f.name !== name)
    setFields(updated)
    await api.updateStack(stack.name, { fields: updated })
    onUpdated()
  }

  // ── Drag reorder ──
  function handleDragStart(e, idx) {
    setDragIdx(idx)
    e.dataTransfer.effectAllowed = 'move'
  }

  function handleDragOver(e, idx) {
    e.preventDefault()
    setDragOverIdx(idx)
  }

  async function handleDrop(e, toIdx) {
    e.preventDefault()
    if (dragIdx === null || dragIdx === toIdx) { setDragIdx(null); setDragOverIdx(null); return }
    const updated = [...fields]
    const [moved] = updated.splice(dragIdx, 1)
    updated.splice(toIdx, 0, moved)
    setFields(updated)
    setDragIdx(null); setDragOverIdx(null)
    await api.updateStack(stack.name, { fields: updated })
    onUpdated()
  }

  // ── Source drag-to-reorder ──
  function handleSrcDragStart(e, idx) {
    setSrcDragIdx(idx)
    e.dataTransfer.effectAllowed = 'move'
  }

  function handleSrcDragOver(e, idx) {
    e.preventDefault()
    setSrcDragOverIdx(idx)
  }

  async function handleSrcDrop(e, toIdx) {
    e.preventDefault()
    if (srcDragIdx === null || srcDragIdx === toIdx) { setSrcDragIdx(null); setSrcDragOverIdx(null); return }
    const updated = [...members]
    const [moved] = updated.splice(srcDragIdx, 1)
    updated.splice(toIdx, 0, moved)
    setSrcDragIdx(null); setSrcDragOverIdx(null)
    await api.reorderStackSources(stack.name, updated.map(m => m.source_name))
    onUpdated()
  }

  // ── Mapping grid ──
  function getMappingValue(srcName, canonicalName) {
    const cfg = srcCfg[srcName] || {}
    if (canonicalName === amountCanonical) return cfg.amount_field || ''
    if (canonicalName === dateCanonical)   return cfg.date_field   || ''
    return cfg.field_map?.[canonicalName] || ''
  }

  function setMappingValue(srcName, canonicalName, value) {
    setSrcCfg(prev => {
      const cfg = { ...prev[srcName] }
      if (canonicalName === amountCanonical)      cfg.amount_field = value
      else if (canonicalName === dateCanonical)   cfg.date_field   = value
      else cfg.field_map = { ...cfg.field_map, [canonicalName]: value }
      return { ...prev, [srcName]: cfg }
    })
    setMappingsDirty(true)
  }

  function setSrcSign(srcName, sign) {
    setSrcCfg(prev => ({ ...prev, [srcName]: { ...prev[srcName], sign } }))
    setMappingsDirty(true)
  }

  function setSrcOffset(srcName, offset) {
    setSrcCfg(prev => ({ ...prev, [srcName]: { ...prev[srcName], offset } }))
    setMappingsDirty(true)
  }

  async function saveMappings() {
    setSaving(true); setError('')
    try {
      for (const m of members) {
        const cfg = srcCfg[m.source_name] || {}
        await api.upsertStackSource(stack.name, m.source_name, {
          field_map:    cfg.field_map    || {},
          amount_sign:  cfg.sign         ?? 1,
          balance_offset: cfg.offset     ?? 0,
          amount_field: cfg.amount_field || null,
          date_field:   cfg.date_field   || null,
        })
      }
      // Persist the auto-detected canonical field names on the stack
      await api.updateStack(stack.name, {
        amount_field: amountCanonical || null,
        date_field:   dateCanonical   || null,
      })
      setMappingsDirty(false)
      onStale?.(stack.name)
      onUpdated()
    } catch (e) { setError(e.message) }
    finally { setSaving(false) }
  }

  // ── Sources ──
  async function addSource() {
    if (!addingSrc) return
    await api.upsertStackSource(stack.name, addingSrc, { field_map: {}, amount_sign: 1 })
    setSrcCfg(prev => ({ ...prev, [addingSrc]: { sign: 1, offset: 0, amount_field: '', date_field: '', field_map: {} } }))
    // Load fields immediately so dropdowns are ready
    try {
      const f = await api.getFields(addingSrc)
      setSrcFields(prev => ({ ...prev, [addingSrc]: f.map(x => x.key) }))
    } catch (e) {}
    setAddingSrc('')
    onStale?.(stack.name)
    onUpdated()
  }

  function handleSrcAmountField(srcName, value) {
    setSrcCfg(prev => ({ ...prev, [srcName]: { ...prev[srcName], amount_field: value } }))
    // Update column type to numeric if a column with this name exists
    setFields(prev => prev.map(f => f.name === value ? { ...f, type: 'numeric' } : f))
    setMappingsDirty(true)
    maybeAutoPopulate(srcName, value, srcCfg[srcName]?.date_field)
  }

  function handleSrcDateField(srcName, value) {
    setSrcCfg(prev => ({ ...prev, [srcName]: { ...prev[srcName], date_field: value } }))
    setFields(prev => prev.map(f => f.name === value ? { ...f, type: 'date' } : f))
    setMappingsDirty(true)
    maybeAutoPopulate(srcName, srcCfg[srcName]?.amount_field, value)
  }

  function maybeAutoPopulate(srcName, amtField, dtField) {
    if (!amtField || !dtField) return
    if (fields.length > 0) return  // don't overwrite existing columns
    const sourceFields = srcFields[srcName] || []
    if (sourceFields.length === 0) return
    const newFields = sourceFields.map(sf => ({
      name: sf,
      type: sf === amtField ? 'numeric' : sf === dtField ? 'date' : 'text',
    }))
    setFields(newFields)
    api.updateStack(stack.name, { fields: newFields, amount_field: amtField, date_field: dtField })
  }

  async function removeSource(src) {
    await api.removeStackSource(stack.name, src)
    setSrcCfg(prev => { const n = { ...prev }; delete n[src]; return n })
    onStale?.(stack.name)
    onUpdated()
  }

  async function handleCalibrate(srcName) {
    // Save this source's current config before opening modal
    const cfg = srcCfg[srcName] || {}
    await api.upsertStackSource(stack.name, srcName, {
      field_map:     cfg.field_map    || {},
      amount_sign:   cfg.sign         ?? 1,
      balance_offset: cfg.offset      ?? 0,
      amount_field:  cfg.amount_field || null,
      date_field:    cfg.date_field   || null,
    })
    setCalibratingSource(srcName)
  }

  async function applyCalibration(srcName, offset) {
    const cfg = srcCfg[srcName] || {}
    await api.upsertStackSource(stack.name, srcName, {
      field_map:     cfg.field_map    || {},
      amount_sign:   cfg.sign         ?? 1,
      balance_offset: offset,
      amount_field:  cfg.amount_field || null,
      date_field:    cfg.date_field   || null,
    })
    setSrcCfg(prev => ({ ...prev, [srcName]: { ...prev[srcName], offset } }))
    setCalibratingSource(null)
    onStale?.(stack.name)
    onUpdated()
  }

  // ── View ──
  async function generateView() {
    setViewResult(null); setNetBalance(null); setBalanceError('')
    try {
      const r = await api.generateStackView(stack.name)
      setViewResult(r)
      if (r.success) {
        fetchBalance()
        onViewGenerated?.(stack.name)
        onSqlGenerated?.(r.sql || '')
        ;(r.cascade_stale || []).forEach(n => onStale?.(n))
      }
    } catch (e) { setError(e.message) }
  }

  async function fetchBalance() {
    setBalanceError('')
    try {
      const r = await api.getStackBalance(stack.name)
      if (r.success) setNetBalance(r.balance)
      else setBalanceError(r.error)
    } catch (e) { setBalanceError(e.message) }
  }

  const availableSources = sources.filter(s => !members.find(m => m.source_name === s.name))
  if (addingSrc === '' && availableSources.length === 1) setAddingSrc(availableSources[0].name)

  return (
    <div className="space-y-5">

      {/* Label */}
      <div className="bg-white border border-gray-200 rounded p-4">
        <h3 className="text-sm font-semibold text-gray-700 mb-3">Configuration</h3>
        <div className="flex gap-3 items-end">
          <div className="flex-1">
            <label className="text-xs text-gray-500 block mb-1">Label <span className="text-gray-400">(optional)</span></label>
            <input className="w-full border border-gray-200 rounded px-3 py-1.5 text-sm focus:outline-none focus:border-blue-400"
              value={label} onChange={e => setLabel(e.target.value)}
              onKeyDown={e => e.key === 'Enter' && saveLabel()} />
          </div>
          <button onClick={saveLabel} disabled={saving}
            className="text-sm bg-blue-600 text-white px-3 py-1.5 rounded hover:bg-blue-700 disabled:opacity-50">
            {saving ? 'Saving…' : 'Save'}
          </button>
        </div>
        {error && <p className="text-xs text-red-500 mt-2">{error}</p>}
      </div>

      {/* Sources */}
      <div className="bg-white border border-gray-200 rounded p-4">
        <h3 className="text-sm font-semibold text-gray-700 mb-1">Sources</h3>
        <p className="text-xs text-gray-400 mb-3">Each source contributes rows to the combined view. Set the sign to flip the direction of amounts (e.g. credit card charges are positive in the source but should subtract from your balance). The offset adjusts the running balance — use Calibrate to compute it from a known good balance.</p>
        <div className="space-y-2 mb-3">
          {members.map((m, idx) => {
            const cfg = srcCfg[m.source_name] || {}
            const sf = srcFields[m.source_name] || []
            const canCalibrate = !!cfg.amount_field && !!cfg.date_field
            return (
              <div key={m.source_name}
                draggable
                onDragStart={e => handleSrcDragStart(e, idx)}
                onDragOver={e => handleSrcDragOver(e, idx)}
                onDrop={e => handleSrcDrop(e, idx)}
                onDragEnd={() => { setSrcDragIdx(null); setSrcDragOverIdx(null) }}
                className={`border border-gray-100 rounded px-3 py-2 text-xs space-y-2 ${srcDragOverIdx === idx && srcDragIdx !== idx ? 'bg-blue-50' : ''}`}>
                <div className="flex items-center gap-2">
                  <span className="text-gray-300 cursor-grab select-none">⠿</span>
                  <span className="font-medium text-gray-700 flex-1">{m.source_name}</span>
                  <button onClick={() => removeSource(m.source_name)} className="text-red-300 hover:text-red-500">Remove</button>
                </div>
                <div className="grid grid-cols-2 gap-x-4 gap-y-1.5">
                  <div>
                    <label className="text-gray-400 block mb-0.5">Amount field</label>
                    <select value={cfg.amount_field || ''}
                      onChange={e => handleSrcAmountField(m.source_name, e.target.value)}
                      className="w-full border border-gray-200 rounded px-1.5 py-0.5 focus:outline-none focus:border-blue-400">
                      <option value="">— select —</option>
                      {sf.map(f => <option key={f} value={f}>{f}</option>)}
                    </select>
                  </div>
                  <div>
                    <label className="text-gray-400 block mb-0.5">Sign</label>
                    <select value={cfg.sign ?? 1}
                      onChange={e => { setSrcSign(m.source_name, parseInt(e.target.value)); setMappingsDirty(true) }}
                      className="w-full border border-gray-200 rounded px-1.5 py-0.5 focus:outline-none focus:border-blue-400">
                      <option value={1}>+1 (as-is)</option>
                      <option value={-1}>−1 (flip)</option>
                    </select>
                  </div>
                  <div>
                    <label className="text-gray-400 block mb-0.5">Date field</label>
                    <select value={cfg.date_field || ''}
                      onChange={e => handleSrcDateField(m.source_name, e.target.value)}
                      className="w-full border border-gray-200 rounded px-1.5 py-0.5 focus:outline-none focus:border-blue-400">
                      <option value="">— select —</option>
                      {sf.map(f => <option key={f} value={f}>{f}</option>)}
                    </select>
                  </div>
                  <div>
                    <label className="text-gray-400 block mb-0.5">Balance offset</label>
                    <div className="flex items-center gap-1">
                      <input type="number" step="0.01" value={cfg.offset ?? 0}
                        onChange={e => { setSrcOffset(m.source_name, parseFloat(e.target.value) || 0); setMappingsDirty(true) }}
                        className="flex-1 border border-gray-200 rounded px-1.5 py-0.5 font-mono focus:outline-none focus:border-blue-400" />
                      <button onClick={() => handleCalibrate(m.source_name)}
                        disabled={!canCalibrate}
                        title={!canCalibrate ? 'Set amount and date fields first' : 'Calibrate balance'}
                        className="text-blue-400 hover:text-blue-600 underline disabled:opacity-40 disabled:cursor-not-allowed disabled:no-underline">
                        Calibrate
                      </button>
                    </div>
                  </div>
                </div>
              </div>
            )
          })}
          {members.length === 0 && <p className="text-xs text-gray-400">No sources added yet.</p>}
        </div>
        {availableSources.length > 0 && (
          <div className="flex gap-2">
            <select className="flex-1 border border-gray-200 rounded px-2 py-1 text-sm focus:outline-none focus:border-blue-400"
              value={addingSrc} onChange={e => setAddingSrc(e.target.value)}>
              <option value="">— add source —</option>
              {availableSources.map(s => <option key={s.name} value={s.name}>{s.name}</option>)}
            </select>
            <button onClick={addSource} disabled={!addingSrc}
              className="text-sm bg-gray-100 px-3 py-1 rounded hover:bg-gray-200 text-gray-700 disabled:opacity-40">Add</button>
          </div>
        )}
      </div>

      {/* Output columns mapping grid */}
      <div className="bg-white border border-gray-200 rounded p-4">
        <h3 className="text-sm font-semibold text-gray-700 mb-1">Output columns</h3>
        <p className="text-xs text-gray-400 mb-3">
          Each row is a column in the combined view. Each source column shows which field from that source maps to it.
          The first <span className="text-blue-500">numeric</span> field drives the running balance; the first <span className="text-green-600">date</span> field drives the ordering.
          Both <span className="font-mono">source_balance</span> (per-source) and <span className="font-mono">net_balance</span> (combined) are always included in the generated view.
          Drag rows to reorder.
        </p>

        {members.length === 0 ? (
          <p className="text-xs text-gray-400 mb-3">Add sources above first.</p>
        ) : (
          <div className="overflow-x-auto mb-3">
            <table className="w-full text-xs border-collapse">
              <thead>
                <tr className="border-b border-gray-200">
                  <th className="w-5 pb-2"></th>
                  <th className="text-left text-gray-400 font-normal pb-2 pr-4">Column</th>
                  <th className="text-left text-gray-400 font-normal pb-2 pr-4">Type</th>
                  {members.map(m => (
                    <th key={m.source_name} className="text-left text-gray-400 font-normal pb-2 pr-3 min-w-36">{m.source_name}</th>
                  ))}
                  <th className="w-5 pb-2"></th>
                </tr>
              </thead>
              <tbody>
                {fields.map((f, idx) => {
                  const isAmount = f.name === amountCanonical
                  const isDate   = f.name === dateCanonical
                  return (
                    <tr key={f.name}
                      draggable
                      onDragStart={e => handleDragStart(e, idx)}
                      onDragOver={e => handleDragOver(e, idx)}
                      onDrop={e => handleDrop(e, idx)}
                      onDragEnd={() => { setDragIdx(null); setDragOverIdx(null) }}
                      className={`border-b border-gray-50 ${dragOverIdx === idx && dragIdx !== idx ? 'bg-blue-50' : ''}`}>
                      <td className="py-1.5 pr-1 text-gray-300 cursor-grab select-none">⠿</td>
                      <td className="py-1.5 pr-4 font-mono text-gray-700 whitespace-nowrap">
                        {f.name}
                        {isAmount && <span className="ml-1.5 text-blue-500 font-sans font-normal">amount</span>}
                        {isDate   && <span className="ml-1.5 text-green-600 font-sans font-normal">date</span>}
                      </td>
                      <td className="py-1.5 pr-4 text-gray-400">{f.type}</td>
                      {members.map(m => (
                        <td key={m.source_name} className="py-1.5 pr-3">
                          <div className="flex items-center gap-1">
                            <select
                              value={getMappingValue(m.source_name, f.name)}
                              onChange={e => setMappingValue(m.source_name, f.name, e.target.value)}
                              className="border border-gray-200 rounded px-1.5 py-0.5 focus:outline-none focus:border-blue-400 min-w-0 flex-1">
                              <option value="">— same name —</option>
                              {(srcFields[m.source_name] || []).map(sf => (
                                <option key={sf} value={sf}>{sf}</option>
                              ))}
                            </select>
                          </div>
                        </td>
                      ))}
                      <td className="py-1.5">
                        <button onClick={() => removeField(f.name)} className="text-red-300 hover:text-red-500">✕</button>
                      </td>
                    </tr>
                  )
                })}
                {fields.length === 0 && (
                  <tr><td colSpan={3 + members.length} className="py-3 text-gray-400 text-center">No columns defined yet — add one below.</td></tr>
                )}
              </tbody>
            </table>
          </div>
        )}

        {/* Add field */}
        <div className="flex gap-2 mb-3">
          <input className="flex-1 border border-gray-200 rounded px-2 py-1 text-sm focus:outline-none focus:border-blue-400"
            placeholder="column name" value={newField.name}
            onChange={e => setNewField(f => ({ ...f, name: e.target.value }))}
            onKeyDown={e => e.key === 'Enter' && addField()} />
          <select className="border border-gray-200 rounded px-2 py-1 text-sm focus:outline-none focus:border-blue-400"
            value={newField.type} onChange={e => setNewField(f => ({ ...f, type: e.target.value }))}>
            {FIELD_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
          </select>
          <button onClick={addField} className="text-sm bg-gray-100 px-3 py-1 rounded hover:bg-gray-200 text-gray-700">Add</button>
        </div>

        {mappingsDirty && (
          <button onClick={saveMappings} disabled={saving}
            className="text-sm bg-blue-600 text-white px-3 py-1.5 rounded hover:bg-blue-700 disabled:opacity-50">
            {saving ? 'Saving…' : 'Save mappings'}
          </button>
        )}
      </div>

      {/* Generate view + balance */}
      <div className="bg-white border border-gray-200 rounded p-4">
        <div className="flex items-center justify-between mb-3">
          <h3 className="text-sm font-semibold text-gray-700">View</h3>
          <div className="flex gap-2">
            <button onClick={fetchBalance}
              className="text-sm bg-gray-100 text-gray-700 px-3 py-1.5 rounded hover:bg-gray-200">
              Refresh balance
            </button>
            <button onClick={generateView}
              className="text-sm bg-blue-600 text-white px-3 py-1.5 rounded hover:bg-blue-700">
              Generate / refresh
            </button>
          </div>
        </div>
        {netBalance !== null && (
          <div className="mb-3 flex items-center gap-3">
            <span className="text-xs text-gray-500">Current net balance</span>
            <span className="text-lg font-mono font-semibold text-gray-800">
              {Number(netBalance).toLocaleString(undefined, { minimumFractionDigits: 2 })}
            </span>
          </div>
        )}
        {balanceError && <p className="text-xs text-gray-400 mb-3">{balanceError}</p>}
        {viewResult && !viewResult.success && (
          <p className="text-xs text-red-500">{viewResult.error}</p>
        )}
        {viewResult && viewResult.success && (
          <p className="text-xs text-green-600">View created: <span className="font-mono">{viewResult.view}</span></p>
        )}
      </div>

      {calibratingSource && (
        <CalibrateModal
          stack={stack}
          sourceName={calibratingSource}
          currentOffset={srcCfg[calibratingSource]?.offset ?? 0}
          onClose={() => setCalibratingSource(null)}
          onApply={offset => applyCalibration(calibratingSource, offset)}
        />
      )}
    </div>
  )
}

// ── Main page ──────────────────────────────────────────────────────────────────

export default function Stacks({ sources, onStackStale, onStackViewGenerated }) {
  const [stacks, setStacks] = useState([])
  const [selected, setSelected] = useState(null)
  const [stackDetail, setStackDetail] = useState(null)
  const [creating, setCreating] = useState(false)
  const [newName, setNewName] = useState('')
  const [error, setError] = useState('')
  const [sqlDraft, setSqlDraft] = useState('')
  const [sqlRunning, setSqlRunning] = useState(false)
  const [sqlResult, setSqlResult] = useState(null)

  async function load() {
    const s = await api.getStacks()
    setStacks(s)
    return s
  }

  async function loadDetail(name) {
    const s = await api.getStack(name)
    setStackDetail(s)
    setSelected(name)
    localStorage.setItem('stacks_last_selected', name)
    setSqlDraft('')
    setSqlResult(null)
  }

  useEffect(() => {
    load().then(s => {
      const last = localStorage.getItem('stacks_last_selected')
      if (last && s.find(x => x.name === last)) loadDetail(last)
    })
  }, [])
  useEffect(() => { if (selected) loadDetail(selected) }, [selected])

  async function createStack() {
    if (!newName) return
    setError('')
    try {
      await api.createStack({ name: newName, fields: [] })
      setNewName(''); setCreating(false)
      await load()
      loadDetail(newName)
    } catch (e) { setError(e.message) }
  }

  async function deleteStack(name) {
    if (!confirm(`Delete stack "${name}"?`)) return
    await api.deleteStack(name)
    if (selected === name) { setSelected(null); setStackDetail(null); setSqlDraft(''); setSqlResult(null) }
    load()
  }

  async function runSql() {
    if (!sqlDraft.trim() || !selected) return
    setSqlRunning(true); setSqlResult(null)
    try {
      const r = await api.execStackSql(selected, sqlDraft)
      setSqlResult(r)
      if (r.success) {
        onStackViewGenerated?.(selected)
        ;(r.cascade_stale || []).forEach(n => onStackStale?.(n))
      }
    } catch (e) { setSqlResult({ success: false, error: e.message }) }
    finally { setSqlRunning(false) }
  }

  return (
    <div className="p-6">
      {/* Stack list — horizontal row of cards */}
      <div className="flex items-center gap-2 mb-5 flex-wrap">
        <h1 className="text-sm font-semibold text-gray-800 mr-1">Stacks</h1>
        {stacks.map(s => (
          <div key={s.name}
            onClick={() => loadDetail(s.name)}
            className={`flex items-center gap-2 px-3 py-1.5 rounded border cursor-pointer text-xs group transition-colors ${selected === s.name ? 'border-blue-300 bg-blue-50 text-blue-700' : 'border-gray-200 bg-white text-gray-600 hover:border-gray-300 hover:bg-gray-50'}`}>
            <span className="font-medium">{s.label || s.name}</span>
            <span className="text-gray-400">{s.source_count}s</span>
            <button onClick={e => { e.stopPropagation(); deleteStack(s.name) }}
              className="opacity-0 group-hover:opacity-100 text-red-300 hover:text-red-500 leading-none">✕</button>
          </div>
        ))}
        {creating ? (
          <div className="flex items-center gap-1">
            <input autoFocus className="border border-blue-400 rounded px-2 py-1 text-xs focus:outline-none w-32"
              placeholder="stack name" value={newName} onChange={e => setNewName(e.target.value)}
              onKeyDown={e => { if (e.key === 'Enter') createStack(); if (e.key === 'Escape') setCreating(false) }} />
            <button onClick={createStack} className="text-xs bg-blue-600 text-white px-2 py-1 rounded">Create</button>
            <button onClick={() => setCreating(false)} className="text-xs text-gray-400 px-1">✕</button>
            {error && <p className="text-xs text-red-500">{error}</p>}
          </div>
        ) : (
          <button onClick={() => setCreating(true)} className="text-xs text-blue-500 hover:text-blue-700 px-2 py-1.5">+ New</button>
        )}
      </div>

      {stackDetail ? (
        <div className="flex gap-6 items-start">
          {/* Left: config panel */}
          <div className="flex-1 min-w-0">
            <h2 className="text-base font-semibold text-gray-800 mb-4">
              {stackDetail.label || stackDetail.name}
              {stackDetail.label && <span className="text-sm text-gray-400 font-normal ml-2">{stackDetail.name}</span>}
            </h2>
            <StackPanel
              key={stackDetail.name}
              stack={stackDetail}
              sources={sources}
              onUpdated={() => { load(); loadDetail(stackDetail.name) }}
              onStale={onStackStale}
              onViewGenerated={onStackViewGenerated}
              onSqlGenerated={sql => { setSqlDraft(prettySql(sql)); setSqlResult(null) }}
            />
          </div>

          {/* Right: SQL panel */}
          <div className="flex-1 min-w-0">
            <div className="bg-white border border-gray-200 rounded p-4 sticky top-4">
              <div className="flex items-center justify-between mb-3">
                <h3 className="text-sm font-semibold text-gray-700">Generated SQL</h3>
                <button
                  onClick={runSql}
                  disabled={!sqlDraft.trim() || sqlRunning}
                  className="text-sm bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700 disabled:opacity-40">
                  {sqlRunning ? 'Running…' : 'Run'}
                </button>
              </div>
              {sqlDraft ? (
                <textarea
                  className="w-full font-mono text-xs text-gray-700 bg-gray-50 border border-gray-200 rounded p-2 focus:outline-none focus:border-blue-400 resize-none leading-relaxed"
                  style={{ minHeight: '60vh' }}
                  value={sqlDraft}
                  onChange={e => { setSqlDraft(e.target.value); setSqlResult(null) }}
                  spellCheck={false}
                />
              ) : (
                <p className="text-xs text-gray-400">Generate a view to see the SQL here.</p>
              )}
              {sqlResult && (
                <p className={`text-xs mt-2 ${sqlResult.success ? 'text-green-600' : 'text-red-500'}`}>
                  {sqlResult.success ? 'View updated successfully.' : sqlResult.error}
                </p>
              )}
            </div>
          </div>
        </div>
      ) : (
        <p className="text-sm text-gray-400">Select a stack or create one.</p>
      )}
    </div>
  )
}
