import { useState, useEffect, useRef } from 'react'
import { api, authHeaders } from '../api'

function AutocompleteInput({ value, onChange, onEnter, suggestions = [], className, placeholder }) {
  const [open, setOpen] = useState(false)
  const [highlighted, setHighlighted] = useState(0)
  const [dropPos, setDropPos] = useState(null)
  const inputRef = useRef()
  const listRef = useRef()

  const filtered = value
    ? suggestions.filter(s => s.toLowerCase().includes(value.toLowerCase()))
    : suggestions

  function openList() {
    if (inputRef.current) {
      const r = inputRef.current.getBoundingClientRect()
      setDropPos({ top: r.bottom + 2, left: r.left, minWidth: r.width })
    }
    setOpen(true)
    setHighlighted(0)
  }

  function select(val) {
    onChange(val)
    setOpen(false)
    inputRef.current?.focus()
  }

  function handleKeyDown(e) {
    if (e.altKey && e.key === 'ArrowDown') {
      e.preventDefault()
      openList()
      return
    }
    if (open && filtered.length > 0) {
      if (e.key === 'Tab') {
        e.preventDefault()
        setHighlighted(h => (h + 1) % filtered.length)
        return
      }
      if (e.key === 'ArrowDown') { e.preventDefault(); setHighlighted(h => Math.min(h + 1, filtered.length - 1)); return }
      if (e.key === 'ArrowUp')   { e.preventDefault(); setHighlighted(h => Math.max(h - 1, 0)); return }
      if (e.key === 'Enter')     { e.preventDefault(); select(filtered[highlighted]); return }
      if (e.key === 'Escape')    { setOpen(false); return }
    }
    if (e.key === 'Enter') onEnter?.()
  }

  useEffect(() => {
    if (!open || !listRef.current) return
    const item = listRef.current.children[highlighted]
    item?.scrollIntoView({ block: 'nearest' })
  }, [highlighted, open])

  return (
    <div className="relative">
      <input
        ref={inputRef}
        className={className}
        value={value}
        placeholder={placeholder}
        onChange={e => { onChange(e.target.value); if (!open && e.target.value) openList() }}
        onKeyDown={handleKeyDown}
        onBlur={e => { if (!listRef.current?.contains(e.relatedTarget)) setOpen(false) }}
      />
      {open && filtered.length > 0 && dropPos && (
        <div
          ref={listRef}
          style={{ position: 'fixed', top: dropPos.top, left: dropPos.left, minWidth: dropPos.minWidth, zIndex: 9999 }}
          className="bg-white border border-gray-200 rounded shadow-lg max-h-48 overflow-y-auto"
        >
          {filtered.map((s, i) => (
            <div
              key={s}
              className={`px-2 py-1 text-xs cursor-pointer whitespace-nowrap ${
                i === highlighted ? 'bg-blue-50 text-blue-700' : 'text-gray-700 hover:bg-gray-50'
              }`}
              onMouseDown={e => { e.preventDefault(); select(s) }}
            >
              {s}
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

function valueKey(v) {
  return Array.isArray(v) ? JSON.stringify(v) : String(v)
}

function displayValue(v) {
  if (Array.isArray(v)) return v.join(' · ')
  return String(v ?? '')
}

function SortHeader({ col, label, sortBy, onSort, className = '' }) {
  const active = sortBy?.col === col
  return (
    <th
      className={`px-3 py-2 font-medium cursor-pointer select-none hover:text-gray-600 ${className}`}
      onClick={() => onSort(col)}
    >
      {label}
      <span className="ml-1 text-gray-300">{active ? (sortBy.dir === 'asc' ? '↑' : '↓') : '↕'}</span>
    </th>
  )
}

export default function Mappings({ source, onNeedsReprocess }) {
  const [rules, setRules] = useState([])
  const [selectedRule, setSelectedRule] = useState('')
  const [allValues, setAllValues] = useState([])
  const [filter, setFilter] = useState('all')
  const [drafts, setDrafts] = useState({})
  const [extraCols, setExtraCols] = useState([])
  const [saving, setSaving] = useState({})
  const [sampleOpen, setSampleOpen] = useState({})
  const [loading, setLoading] = useState(false)
  const [importing, setImporting] = useState(false)
  const [sortBy, setSortBy] = useState(null)
  const [globalValues, setGlobalValues] = useState({})
  const [selected, setSelected] = useState(new Set())
  const [bulkDraft, setBulkDraft] = useState({})
  const [cursorKey, setCursorKey] = useState(null)
  const [rowFilter, setRowFilter] = useState('')
  const rowRefs = useRef({})

  useEffect(() => {
    if (!source) return
    api.getGlobalValues().then(setGlobalValues).catch(() => {})
    api.getRules(source).then(r => setRules(r)).catch(() => {})
  }, [source])

  useEffect(() => {
    if (!source || !selectedRule) {
      setAllValues([])
      return
    }
    setLoading(true)
    api.getAllValues(source, selectedRule)
      .then(a => {
        setAllValues(a)
        setDrafts({})
        setExtraCols([])
        setSelected(new Set())
        setBulkDraft({})
        setCursorKey(null)
        setRowFilter('')
      })
      .catch(() => {})
      .finally(() => setLoading(false))
  }, [source, selectedRule])

  // Auto-select all rows matching the regex filter when it changes
  useEffect(() => {
    if (!rowFilter) return
    let re = null
    try { re = new RegExp(rowFilter, 'i') } catch { return }
    const tabF = filter === 'unmapped' ? allValues.filter(r => !r.is_mapped)
               : filter === 'mapped'   ? allValues.filter(r =>  r.is_mapped)
               : allValues
    const matches = tabF.filter(r => re.test(displayValue(r.extracted_value)))
    setSelected(new Set(matches.map(r => valueKey(r.extracted_value))))
  }, [rowFilter, filter, allValues])

  // Derive output columns and datalist suggestions from mapped rows + global pool
  const existingCols = []
  const valuesByCol = {}
  allValues.forEach(row => {
    if (!row.is_mapped) return
    Object.entries(row.output || {}).forEach(([k, v]) => {
      if (!existingCols.includes(k)) existingCols.push(k)
      if (!valuesByCol[k]) valuesByCol[k] = new Set()
      valuesByCol[k].add(String(v))
    })
  })
  // Merge global picklist values into suggestions
  Object.entries(globalValues).forEach(([k, vals]) => {
    if (!valuesByCol[k]) valuesByCol[k] = new Set()
    vals.forEach(v => valuesByCol[k].add(v))
  })
  const cols = [...existingCols, ...extraCols]

  const unmappedCount = allValues.filter(r => !r.is_mapped).length
  const mappedCount = allValues.filter(r => r.is_mapped).length

  const tabFiltered = filter === 'unmapped'
    ? allValues.filter(r => !r.is_mapped)
    : filter === 'mapped'
    ? allValues.filter(r => r.is_mapped)
    : allValues

  let rowFilterRe = null
  let rowFilterError = false
  if (rowFilter) {
    try { rowFilterRe = new RegExp(rowFilter, 'i') } catch { rowFilterError = true }
  }
  const filteredRows = rowFilterRe
    ? tabFiltered.filter(r => rowFilterRe.test(displayValue(r.extracted_value)))
    : tabFiltered

  function toggleSort(col) {
    setSortBy(s => {
      if (s?.col === col) return { col, dir: s.dir === 'asc' ? 'desc' : 'asc' }
      return { col, dir: 'asc' }
    })
  }

  function sortedRows(rows) {
    if (!sortBy) return rows
    return [...rows].sort((a, b) => {
      if (sortBy.col === 'count') {
        const av = Number(a.record_count) || 0
        const bv = Number(b.record_count) || 0
        return sortBy.dir === 'asc' ? av - bv : bv - av
      }
      let av, bv
      if (sortBy.col === 'input_value') {
        av = displayValue(a.extracted_value)
        bv = displayValue(b.extracted_value)
      } else {
        av = a.is_mapped ? String(a.output?.[sortBy.col] ?? '') : ''
        bv = b.is_mapped ? String(b.output?.[sortBy.col] ?? '') : ''
      }
      return sortBy.dir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av)
    })
  }

  function setCellValue(extractedValue, col, value) {
    const k = valueKey(extractedValue)
    const targets = selected.has(k) && selected.size > 1 ? [...selected] : [k]
    setDrafts(d => {
      const next = { ...d }
      for (const sk of targets) next[sk] = { ...(next[sk] || {}), [col]: value }
      return next
    })
  }

  async function saveRow(row) {
    const k = valueKey(row.extracted_value)
    const output = Object.fromEntries(
      cols.map(col => {
        const drafted = drafts[k]?.[col]
        const val = drafted !== undefined ? drafted : (row.is_mapped ? String(row.output?.[col] ?? '') : '')
        return [col, val]
      }).filter(([, v]) => v.trim())
    )
    if (Object.keys(output).length === 0) return

    setSaving(s => ({ ...s, [k]: true }))
    try {
      if (row.is_mapped && row.mapping_id) {
        const updated = await api.updateMapping(row.mapping_id, { output })
        setAllValues(av => av.map(x =>
          valueKey(x.extracted_value) === k ? { ...x, output: updated.output } : x
        ))
      } else {
        const created = await api.createMapping({
          source_name: source,
          rule_name: row.rule_name,
          input_value: row.extracted_value,
          output
        })
        setAllValues(av => av.map(x =>
          valueKey(x.extracted_value) === k ? { ...x, is_mapped: true, mapping_id: created.id, output } : x
        ))
      }
      onNeedsReprocess?.(source)
      setDrafts(d => { const n = { ...d }; delete n[k]; return n })
    } catch (err) {
      alert(err.message)
    } finally {
      setSaving(s => ({ ...s, [k]: false }))
    }
  }

  const dirtyCount = filteredRows.filter(row => {
    const k = valueKey(row.extracted_value)
    return drafts[k] && Object.keys(drafts[k]).length > 0
  }).length

  async function saveAllPending() {
    const dirty = filteredRows.filter(row => {
      const k = valueKey(row.extracted_value)
      return drafts[k] && Object.keys(drafts[k]).length > 0
    })
    await Promise.all(dirty.map(row => saveRow(row)))
    setRowFilter('')
  }

  async function applyBulk() {
    const output = Object.fromEntries(
      Object.entries(bulkDraft).filter(([, v]) => v.trim())
    )
    if (Object.keys(output).length === 0) return
    const rows = sortedRows(filteredRows).filter(r => selected.has(valueKey(r.extracted_value)))
    await Promise.all(rows.map(async row => {
      const k = valueKey(row.extracted_value)
      const merged = { ...(row.is_mapped ? row.output : {}), ...output }
      setSaving(s => ({ ...s, [k]: true }))
      try {
        if (row.is_mapped && row.mapping_id) {
          const updated = await api.updateMapping(row.mapping_id, { output: merged })
          setAllValues(av => av.map(x => valueKey(x.extracted_value) === k ? { ...x, output: updated.output } : x))
        } else {
          const created = await api.createMapping({ source_name: source, rule_name: row.rule_name, input_value: row.extracted_value, output: merged })
          setAllValues(av => av.map(x => valueKey(x.extracted_value) === k ? { ...x, is_mapped: true, mapping_id: created.id, output: merged } : x))
        }
      } catch (err) {
        alert(err.message)
      } finally {
        setSaving(s => ({ ...s, [k]: false }))
      }
    }))
    onNeedsReprocess?.(source)
    setSelected(new Set())
    setBulkDraft({})
  }

  async function deleteRow(row) {
    if (!row.mapping_id) return
    try {
      await api.deleteMapping(row.mapping_id)
      onNeedsReprocess?.(source)
      setAllValues(av => av.map(x =>
        valueKey(x.extracted_value) === valueKey(row.extracted_value)
          ? { ...x, is_mapped: false, mapping_id: null, output: null }
          : x
      ))
    } catch (err) {
      alert(err.message)
    }
  }

  async function handleImportCSV(e) {
    const file = e.target.files[0]
    if (!file) return
    e.target.value = ''
    setImporting(true)
    try {
      const result = await api.importMappingsCSV(source, file)
      alert(`Imported ${result.count} mapping${result.count !== 1 ? 's' : ''}.`)
      const a = await api.getAllValues(source, selectedRule)
      setAllValues(a)
      setDrafts({})
      setExtraCols([])
    } catch (err) {
      alert(err.message)
    } finally {
      setImporting(false)
    }
  }

  if (!source) return <div className="p-6 text-sm text-gray-400">Select a source first.</div>

  const displayRows = sortedRows(filteredRows)

  return (
    <div>
      {/* Sticky control bar */}
      <div className="sticky top-0 z-10 bg-white border-b border-gray-200 px-6 py-3 flex items-center gap-3 flex-wrap">
        <span className="text-sm font-medium text-gray-700">{source}</span>

        <select
          className="text-sm border border-gray-200 rounded px-2 py-1.5 focus:outline-none focus:border-blue-400"
          value={selectedRule}
          onChange={e => setSelectedRule(e.target.value)}
        >
          <option value="">Select a rule…</option>
          {rules.map(r => <option key={r.name} value={r.name}>{r.name}</option>)}
        </select>

        {selectedRule && (
          <div className="flex bg-gray-100 rounded p-0.5">
            {[
              { key: 'all', label: `All (${allValues.length})` },
              { key: 'unmapped', label: `Unmapped (${unmappedCount})` },
              { key: 'mapped', label: `Mapped (${mappedCount})` },
            ].map(({ key, label }) => (
              <button key={key} onClick={() => setFilter(key)}
                className={`text-xs px-3 py-1 rounded transition-colors ${
                  filter === key ? 'bg-white text-gray-800 shadow-sm' : 'text-gray-500'
                }`}>
                {label}
              </button>
            ))}
          </div>
        )}

        {selectedRule && (
          <div className="relative">
            <input
              className={`text-xs font-mono border rounded px-2 py-1.5 w-44 focus:outline-none focus:border-blue-400 ${
                rowFilterError ? 'border-red-400 bg-red-50' : rowFilter ? 'border-blue-300' : 'border-gray-200'
              }`}
              placeholder="filter regex…"
              value={rowFilter}
              onChange={e => setRowFilter(e.target.value)}
            />
            {rowFilter && !rowFilterError && (
              <span className="absolute right-2 top-1/2 -translate-y-1/2 text-xs text-gray-400">
                {filteredRows.length}
              </span>
            )}
          </div>
        )}

        {dirtyCount > 0 && (
          <button
            onClick={saveAllPending}
            className="text-sm bg-blue-600 text-white px-3 py-1.5 rounded hover:bg-blue-700"
          >
            Save all ({dirtyCount})
          </button>
        )}

        <div className="ml-auto flex items-center gap-2">
          {selectedRule && (
            <button
              onClick={async () => {
                try {
                  const url = api.exportMappingsUrl(source, selectedRule)
                  const res = await fetch(url, { headers: authHeaders() })
                  if (!res.ok) throw new Error('Export failed')
                  const blob = await res.blob()
                  const a = document.createElement('a')
                  a.href = URL.createObjectURL(blob)
                  a.download = `mappings_${source}.tsv`
                  a.click()
                  URL.revokeObjectURL(a.href)
                } catch (err) {
                  alert(err.message)
                }
              }}
              className="text-sm px-3 py-1.5 border border-gray-200 rounded hover:bg-gray-50 text-gray-600"
            >
              Export TSV
            </button>
          )}
          <label className={`text-sm px-3 py-1.5 border border-gray-200 rounded cursor-pointer hover:bg-gray-50 text-gray-600 ${importing ? 'opacity-50 pointer-events-none' : ''}`}>
            {importing ? 'Importing…' : 'Import TSV'}
            <input type="file" accept=".tsv,.txt" className="hidden" onChange={handleImportCSV} />
          </label>
        </div>
      </div>

      {/* Content */}
      <div className="p-6">
        {!selectedRule && (
          <p className="text-sm text-gray-400">Select a rule to view mappings.</p>
        )}
        {selectedRule && loading && (
          <p className="text-sm text-gray-400">Loading…</p>
        )}
        {selectedRule && !loading && allValues.length === 0 && (
          <p className="text-sm text-gray-400">No extracted values for this rule. Run a transform first.</p>
        )}
        {selectedRule && !loading && allValues.length > 0 && (
          <div className="overflow-x-auto">
            {/* Bulk assign bar */}
            {selected.size > 0 && (
              <div className="flex items-center gap-2 mb-2 p-2 bg-blue-50 border border-blue-200 rounded flex-wrap">
                <span className="text-xs text-blue-700 font-medium whitespace-nowrap">{selected.size} selected</span>
                {cols.map(col => (
                  <AutocompleteInput
                    key={col}
                    className="border border-blue-300 rounded px-2 py-1 text-xs min-w-24 focus:outline-none focus:border-blue-500 bg-white"
                    placeholder={col}
                    value={bulkDraft[col] || ''}
                    onChange={v => setBulkDraft(d => ({ ...d, [col]: v }))}
                    suggestions={[...(valuesByCol[col] || [])].sort()}
                  />
                ))}
                <button
                  onClick={applyBulk}
                  disabled={Object.values(bulkDraft).every(v => !v.trim())}
                  className="text-xs bg-blue-600 text-white px-3 py-1 rounded hover:bg-blue-700 disabled:opacity-40 whitespace-nowrap"
                >
                  Apply to {selected.size}
                </button>
                <button
                  onClick={() => { setSelected(new Set()); setBulkDraft({}) }}
                  className="text-xs text-blue-400 hover:text-blue-600"
                >
                  cancel
                </button>
              </div>
            )}
            <table className="w-full text-xs bg-white border border-gray-200 rounded">
              <thead>
                <tr className="text-left text-gray-400 border-b border-gray-100 bg-gray-50">
                  <th className="px-2 py-2 w-6">
                    <input
                      type="checkbox"
                      className="cursor-pointer"
                      checked={displayRows.length > 0 && displayRows.every(r => selected.has(valueKey(r.extracted_value)))}
                      onChange={e => {
                        if (e.target.checked) setSelected(new Set(displayRows.map(r => valueKey(r.extracted_value))))
                        else setSelected(new Set())
                      }}
                    />
                  </th>
                  <SortHeader col="input_value" label="input_value" sortBy={sortBy} onSort={toggleSort} />
                  <SortHeader col="count" label="count" sortBy={sortBy} onSort={toggleSort} className="text-right" />
                  {existingCols.map(col => (
                    <SortHeader key={col} col={col} label={col} sortBy={sortBy} onSort={toggleSort} />
                  ))}
                  {extraCols.map((col, idx) => (
                    <th key={`extra-${idx}`} className="px-3 py-2 font-medium">
                      <input
                        className="border border-gray-200 rounded px-1 py-0.5 w-24 focus:outline-none focus:border-blue-400 font-normal"
                        value={col}
                        placeholder="new key"
                        onChange={e => setExtraCols(ec => { const c = [...ec]; c[idx] = e.target.value; return c })}
                      />
                    </th>
                  ))}
                  <th className="px-2 py-2">
                    <button
                      onClick={() => setExtraCols(ec => [...ec, ''])}
                      className="text-gray-400 hover:text-gray-700 font-medium"
                      title="Add column"
                    >+</button>
                  </th>
                  <th className="px-3 py-2"></th>
                  <th className="px-3 py-2"></th>
                </tr>
              </thead>
              <tbody>
                {displayRows.map(row => {
                  const k = valueKey(row.extracted_value)
                  const rowIdx = displayRows.indexOf(row)
                  const isSaving = saving[k]
                  const isSelected = selected.has(k)
                  const hasDraft = !!(drafts[k] && Object.keys(drafts[k]).length > 0)
                  const rowBg = isSelected ? 'bg-blue-50' : hasDraft ? 'bg-blue-50' : row.is_mapped ? '' : 'bg-yellow-50'

                  function handleRowClick(e) {
                    if (e.target.closest('input,button,a,select')) return
                    setSelected(s => { const n = new Set(s); n.has(k) ? n.delete(k) : n.add(k); return n })
                    setCursorKey(k)
                  }

                  function handleRowKeyDown(e) {
                    if (!e.shiftKey || (e.key !== 'ArrowDown' && e.key !== 'ArrowUp')) return
                    e.preventDefault()
                    const delta = e.key === 'ArrowDown' ? 1 : -1
                    const curIdx = cursorKey ? displayRows.findIndex(r => valueKey(r.extracted_value) === cursorKey) : rowIdx
                    const nextIdx = Math.max(0, Math.min(displayRows.length - 1, curIdx + delta))
                    const nextKey = valueKey(displayRows[nextIdx].extracted_value)
                    setSelected(s => new Set([...s, nextKey]))
                    setCursorKey(nextKey)
                    rowRefs.current[nextKey]?.focus()
                  }
                  const samples = row.sample
                    ? (Array.isArray(row.sample) ? row.sample : [row.sample])
                    : []

                  const cellVal = col => {
                    const drafted = drafts[k]?.[col]
                    if (drafted !== undefined) return drafted
                    return row.is_mapped ? String(row.output?.[col] ?? '') : ''
                  }

                  return (
                    <>
                      <tr
                        key={k}
                        ref={el => rowRefs.current[k] = el}
                        tabIndex={0}
                        className={`border-t border-gray-50 hover:bg-gray-50 cursor-pointer outline-none ${rowBg}`}
                        onClick={handleRowClick}
                        onKeyDown={handleRowKeyDown}
                      >
                        <td className="px-2 py-1.5">
                          <input
                            type="checkbox"
                            className="cursor-pointer"
                            checked={isSelected}
                            onChange={() => {
                              setSelected(s => { const n = new Set(s); n.has(k) ? n.delete(k) : n.add(k); return n })
                              setCursorKey(k)
                            }}
                          />
                        </td>
                        <td className="px-3 py-1.5 font-mono text-gray-800 whitespace-nowrap">{displayValue(row.extracted_value)}</td>
                        <td className="px-3 py-1.5 text-right text-gray-400">{row.record_count}</td>
                        {cols.map(col => (
                          <td key={col} className="px-3 py-1.5">
                            <AutocompleteInput
                              className={`border rounded px-2 py-1 w-full min-w-24 focus:outline-none focus:border-blue-400 ${
                                hasDraft ? 'border-blue-300' : row.is_mapped ? 'border-gray-200' : 'border-yellow-300'
                              }`}
                              value={cellVal(col)}
                              onChange={v => setCellValue(row.extracted_value, col, v)}
                              onEnter={() => saveRow(row)}
                              suggestions={[...(valuesByCol[col] || [])].sort()}
                            />
                          </td>
                        ))}
                        <td />
                        <td className="px-3 py-1.5 whitespace-nowrap">
                          {samples.length > 0 && (
                            <button
                              className="text-blue-400 hover:text-blue-600"
                              onClick={() => setSampleOpen(s => ({ ...s, [k]: !s[k] }))}
                            >
                              {sampleOpen[k] ? 'hide' : 'show'}
                            </button>
                          )}
                        </td>
                        <td className="px-3 py-1.5">
                          <div className="flex items-center gap-2">
                            <button
                              onClick={() => saveRow(row)}
                              disabled={isSaving}
                              className="bg-blue-600 text-white px-2 py-1 rounded hover:bg-blue-700 disabled:opacity-50 whitespace-nowrap"
                            >
                              {isSaving ? '…' : 'Save'}
                            </button>
                            {row.is_mapped && (
                              <button
                                onClick={() => deleteRow(row)}
                                className="text-red-400 hover:text-red-600 text-base leading-none"
                                title="Remove mapping"
                              >×</button>
                            )}
                          </div>
                        </td>
                      </tr>
                      {sampleOpen[k] && (() => {
                        const sampleCols = [...new Set(samples.flatMap(r => Object.keys(r)))]
                        return (
                          <tr key={`${k}-sample`} className="border-t border-gray-50 bg-gray-50">
                            <td colSpan={3 + cols.length + 4} className="px-3 py-2">
                              <table className="w-full text-xs border border-gray-100 rounded bg-white">
                                <thead>
                                  <tr className="bg-gray-50 border-b border-gray-100">
                                    {sampleCols.map(c => (
                                      <th key={c} className="px-2 py-1 text-left font-medium text-gray-400 whitespace-nowrap">{c}</th>
                                    ))}
                                  </tr>
                                </thead>
                                <tbody>
                                  {samples.map((rec, i) => (
                                    <tr key={i} className="border-t border-gray-50">
                                      {sampleCols.map(c => (
                                        <td key={c} className="px-2 py-1 font-mono text-gray-600 whitespace-nowrap">
                                          {rec[c] != null ? String(rec[c]) : ''}
                                        </td>
                                      ))}
                                    </tr>
                                  ))}
                                </tbody>
                              </table>
                            </td>
                          </tr>
                        )
                      })()}
                    </>
                  )
                })}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </div>
  )
}
