// frontend/src/pages/melodies/MelodyList.jsx

import { useState, useEffect, useRef, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import api from '@/lib/api'
import { useAuth } from '@/hooks/useAuth'
import PageHeader from '@/components/ui/PageHeader'
import Button from '@/components/ui/Button'
import DataTable from '@/components/ui/DataTable'
import StatusBadge from '@/components/ui/StatusBadge'
import Pagination from '@/components/ui/Pagination'
import SearchBar from '@/components/ui/SearchBar'
import Select from '@/components/ui/Select'
import RowActions from '@/components/ui/RowActions'
import Icon from '@/components/ui/Icon'
import ConfirmDialog from '@/components/ui/ConfirmDialog'
import { fmtRelative, fmtDateTime as fmtDateTimeCentral } from '@/lib/formatters'

// ─── Utilities ────────────────────────────────────────────────────────────────

function parseLocalizedString(value) {
  if (!value) return {}
  if (typeof value === 'object') return value
  try {
    const parsed = JSON.parse(value)
    if (typeof parsed === 'object' && parsed !== null) return parsed
  } catch {
    return { en: value }
  }
  return {}
}

function getLocalizedValue(value, lang, fallback = '') {
  const dict = parseLocalizedString(value)
  return dict[lang] || dict['en'] || Object.values(dict)[0] || fallback
}

function normalizeColor(val) {
  if (!val) return null
  if (val.startsWith('0x') || val.startsWith('0X')) {
    const hex = val.slice(2)
    return `#${hex.length === 8 ? hex.slice(2) : hex}`
  }
  if (val.startsWith('#')) return val
  return `#${val}`
}

function formatBpm(ms) {
  const value = Number(ms)
  if (!value || value <= 0) return null
  return Math.round(60000 / value)
}

function formatRelativeTime(isoValue) { return fmtRelative(isoValue) }

function formatDateShort(isoValue) {
  const d = new Date(isoValue)
  if (Number.isNaN(d.getTime())) return { date: '—', time: '—' }
  const dd   = String(d.getDate()).padStart(2, '0')
  const mm   = String(d.getMonth() + 1).padStart(2, '0')
  const yyyy = d.getFullYear()
  const hh   = String(d.getHours()).padStart(2, '0')
  const min  = String(d.getMinutes()).padStart(2, '0')
  return { date: `${dd}/${mm}/${yyyy}`, time: `${hh}:${min}` }
}

function parseDateValue(isoValue) {
  if (!isoValue) return 0
  const t = new Date(isoValue).getTime()
  return Number.isNaN(t) ? 0 : t
}

// ─── Column definitions ───────────────────────────────────────────────────────

const ALL_COLUMNS = [
  { key: 'color',            label: '',                  pickerLabel: 'Colour',         defaultOn: true,  width: '16px'  },
  { key: 'name',             label: 'Name',                                              defaultOn: true,  alwaysOn: true, sortable: true },
  { key: 'status',           label: 'Status',                                            defaultOn: true,  align: 'center' },
  { key: 'type',             label: 'Type',                                              defaultOn: true,  align: 'center' },
  { key: 'tone',             label: 'Tone',                                              defaultOn: true  },
  { key: 'totalActiveBells', label: 'Unique Bells',                                      defaultOn: true,  sortable: true, align: 'center' },
  { key: 'isTrueRing',       label: 'True Ring',                                         defaultOn: true  },
  { key: 'minSpeed',         label: 'Min Speed',                                         defaultOn: false },
  { key: 'maxSpeed',         label: 'Max Speed',                                         defaultOn: false },
  { key: 'tags',             label: 'Tags',                                              defaultOn: false },
  { key: 'description',      label: 'Description',                                       defaultOn: false },
  { key: 'speed',            label: 'Speed',                                             defaultOn: false },
  { key: 'duration',         label: 'Duration',                                          defaultOn: false },
  { key: 'totalRunDuration', label: 'Total Run',                                         defaultOn: false },
  { key: 'pauseDuration',    label: 'Pause',                                             defaultOn: false },
  { key: 'infiniteLoop',     label: 'Infinite',                                          defaultOn: false },
  { key: 'noteAssignments',  label: 'Note Assignments',                                  defaultOn: false },
  { key: 'binaryFile',       label: 'Binary File',                                       defaultOn: false },
  { key: 'dateCreated',      label: 'Created',                                           defaultOn: false, sortable: true },
  { key: 'dateEdited',       label: 'Last Edited',                                       defaultOn: false, sortable: true },
  { key: 'createdBy',        label: 'Created By',                                        defaultOn: false },
  { key: 'lastEditedBy',     label: 'Last Edited By',                                    defaultOn: false },
  { key: 'pid',              label: 'PID',                                               defaultOn: false },
  { key: 'docId',            label: 'Doc ID',                                            defaultOn: false },
]

const STORAGE_KEY = 'melodyListColumnPrefs_v2'

function loadColumnPrefs() {
  try {
    const raw = localStorage.getItem(STORAGE_KEY)
    if (raw) {
      const prefs = JSON.parse(raw)
      if (prefs.order && prefs.visible) return prefs
    }
  } catch { /* ignore */ }
  return {
    order:   ALL_COLUMNS.map((c) => c.key),
    visible: ALL_COLUMNS.filter((c) => c.defaultOn).map((c) => c.key),
  }
}

function saveColumnPrefs(order, visible) {
  try {
    localStorage.setItem(STORAGE_KEY, JSON.stringify({ order, visible }))
  } catch { /* ignore */ }
}

const LANG_NAMES = {
  en: 'English', el: 'Greek', sr: 'Serbian', bg: 'Bulgarian',
  ro: 'Romanian', ru: 'Russian', uk: 'Ukrainian', ar: 'Arabic',
  fr: 'French', de: 'German', es: 'Spanish', it: 'Italian',
}

// ─── Progress bar colour helpers ──────────────────────────────────────────────

function speedBarColor(speedPercent) {
  const v = Math.max(0, Math.min(100, Number(speedPercent || 0)))
  if (v <= 50) {
    const t = v / 50
    const hue = 120 + (210 - 120) * t // green → blue
    return `hsl(${hue}, 85%, 46%)`
  }
  const t = (v - 50) / 50
  const hue = 210 + (0 - 210) * t // blue → red
  return `hsl(${hue}, 85%, 46%)`
}

function durationBarColor(percent) {
  const v = Math.max(0, Math.min(100, Number(percent || 0)))
  const hue = 220 + (0 - 220) * (v / 100) // blue → red
  return `hsl(${hue}, 85%, 46%)`
}

function mapPercentageToStepDelay(percent, minSpeed, maxSpeed) {
  if (minSpeed == null || maxSpeed == null) return null
  const p = Math.max(0, Math.min(100, Number(percent || 0)))
  const t = p / 100
  const a = Number(minSpeed)
  const b = Number(maxSpeed)
  if (a <= 0 || b <= 0) return Math.round(a + (b - a) * t)
  return Math.round(a * Math.pow(b / a, t))
}

function formatDurationVerbose(seconds) {
  const total = Number(seconds || 0)
  const mins  = Math.floor(total / 60)
  const secs  = total % 60
  return `${mins} min ${secs} sec`
}

// ─── DocIdCell — hover highlight + click to copy ─────────────────────────────

function DocIdCell({ id }) {
  const [copied, setCopied] = useState(false)
  const [hovered, setHovered] = useState(false)

  function handleClick(e) {
    e.stopPropagation()
    if (!id) return
    const fn = () => { setCopied(true); setTimeout(() => setCopied(false), 1800) }
    if (navigator.clipboard) {
      navigator.clipboard.writeText(id).then(fn).catch(() => {
        const ta = document.createElement('textarea')
        ta.value = id; ta.style.cssText = 'position:fixed;opacity:0'
        document.body.appendChild(ta); ta.select()
        try { document.execCommand('copy'); fn() } catch (_) {}
        document.body.removeChild(ta)
      })
    }
  }

  return (
    <div style={{ position: 'relative', display: 'inline-block' }}>
      <span
        onClick={handleClick}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
        title="Click to copy Doc ID"
        style={{
          fontFamily: 'var(--font-family-mono)',
          fontSize: 'var(--font-size-xs)',
          color: hovered ? 'var(--color-primary)' : 'var(--color-text-muted)',
          cursor: 'pointer',
          transition: 'color 120ms ease',
          userSelect: 'none',
        }}
      >
        {id || '—'}
      </span>
      {copied && (
        <span style={{
          position: 'absolute',
          bottom: '100%',
          left: '50%',
          transform: 'translateX(-50%)',
          marginBottom: 4,
          backgroundColor: 'var(--color-success)',
          color: '#fff',
          fontSize: 'var(--font-size-xs)',
          fontWeight: 'var(--font-weight-semibold)',
          padding: '2px 8px',
          borderRadius: 'var(--radius-full)',
          whiteSpace: 'nowrap',
          pointerEvents: 'none',
          zIndex: 100,
          animation: 'docid-fade 1.8s ease forwards',
        }}>
          DOC ID Copied !
        </span>
      )}
      <style>{`@keyframes docid-fade { 0%{opacity:1} 70%{opacity:1} 100%{opacity:0} }`}</style>
    </div>
  )
}

// ─── MelodyList ───────────────────────────────────────────────────────────────

export default function MelodyList() {
  const navigate  = useNavigate()
  const { hasPermission } = useAuth()
  const canEdit   = hasPermission('melodies', 'edit')
  const canAdd    = hasPermission('melodies', 'add')

  // Data
  const [melodies, setMelodies]           = useState([])
  const [allCount, setAllCount]           = useState(0)
  const [loading, setLoading]             = useState(true)
  const [error, setError]                 = useState('')
  const [melodySettings, setMelodySettings] = useState(null)

  // Filters
  const [search, setSearch]               = useState('')
  const [typeFilter, setTypeFilter]       = useState('')
  const [toneFilter, setToneFilter]       = useState('')
  const [statusFilter, setStatusFilter]   = useState('')

  // Sort & pagination
  const [sortKey, setSortKey]             = useState('dateCreated')
  const [sortDir, setSortDir]             = useState('desc')
  const [page, setPage]                   = useState(1)
  const [pageSize, setPageSize]           = useState(20)

  // Display
  const [displayLang, setDisplayLang]     = useState('en')

  // Columns
  const [colPrefs, setColPrefs]           = useState(loadColumnPrefs)

  // Actions
  const [deleteTarget, setDeleteTarget]       = useState(null)
  const [unpublishTarget, setUnpublishTarget] = useState(null)
  const [actionLoading, setActionLoading]     = useState(null)

  // Multi-select
  const [multiSelectMode, setMultiSelectMode] = useState(false)
  const [selectedIds, setSelectedIds]         = useState(new Set())
  const [actionsOpen, setActionsOpen]         = useState(false)
  const [bulkDeleteConfirm, setBulkDeleteConfirm] = useState(false)
  const [bulkActionLoading, setBulkActionLoading] = useState(false)
  const actionsBtnRef = useRef(null)
  const actionsMenuRef = useRef(null)

  // Close actions dropdown on outside click
  useEffect(() => {
    if (!actionsOpen) return
    function onMouseDown(e) {
      if (actionsBtnRef.current?.contains(e.target)) return
      if (actionsMenuRef.current?.contains(e.target)) return
      setActionsOpen(false)
    }
    document.addEventListener('mousedown', onMouseDown)
    return () => document.removeEventListener('mousedown', onMouseDown)
  }, [actionsOpen])

  const exitMultiSelect = () => {
    setMultiSelectMode(false)
    setSelectedIds(new Set())
    setActionsOpen(false)
  }

  const toggleSelected = (id) => {
    setSelectedIds((prev) => {
      const next = new Set(prev)
      if (next.has(id)) {
        next.delete(id)
      } else {
        next.add(id)
      }
      // Auto-exit if we had at least 1 selected and now have 0
      if (prev.size > 0 && next.size === 0) {
        setMultiSelectMode(false)
        setActionsOpen(false)
      }
      return next
    })
  }

  // ── Settings ───────────────────────────────────────────────────────────────
  useEffect(() => {
    api.get('/settings/melody').then((ms) => {
      if (ms) {
        setMelodySettings(ms)
        setDisplayLang(ms.primary_language || 'en')
      }
    }).catch(() => {})
  }, [])

  // ── Fetch ──────────────────────────────────────────────────────────────────
  const fetchAllCount = async () => {
    try {
      const allData = await api.get('/melodies')
      setAllCount(allData.total || (allData.melodies || []).length || 0)
    } catch { /* ignore */ }
  }

  const fetchMelodies = async () => {
    setLoading(true)
    setError('')
    try {
      const params = new URLSearchParams()
      if (search)       params.set('search', search)
      if (typeFilter)   params.set('type',   typeFilter)
      if (toneFilter)   params.set('tone',   toneFilter)
      if (statusFilter) params.set('status', statusFilter)
      const qs = params.toString()
      const data = await api.get(`/melodies${qs ? `?${qs}` : ''}`)
      setMelodies(data.melodies || [])
    } catch (err) {
      setError(err.message || 'Failed to load melodies')
    } finally {
      setLoading(false)
    }
  }

  useEffect(() => {
    fetchAllCount()
  }, []) // eslint-disable-line react-hooks/exhaustive-deps

  useEffect(() => {
    fetchMelodies()
    setPage(1)
  }, [search, typeFilter, toneFilter, statusFilter]) // eslint-disable-line react-hooks/exhaustive-deps

  // ── Column management ──────────────────────────────────────────────────────
  const handleColumnChange = (nextVisible) => {
    setColPrefs(() => {
      const newPrefs = { order: nextVisible, visible: nextVisible }
      saveColumnPrefs(newPrefs.order, newPrefs.visible)
      return newPrefs
    })
  }

  // ── Sort ───────────────────────────────────────────────────────────────────
  const getSortValue = (row, key) => {
    const info     = row?.information || {}
    const metadata = row?.metadata    || {}
    switch (key) {
      case 'name':             return getLocalizedValue(info.name, displayLang, 'Untitled').toLowerCase()
      case 'totalActiveBells': return Number(info.totalActiveBells || 0)
      case 'dateCreated':      return parseDateValue(metadata.dateCreated)
      case 'dateEdited':       return parseDateValue(metadata.dateEdited)
      default:                 return 0
    }
  }

  const handleSort = (key, dir) => {
    setSortKey(key)
    setSortDir(dir)
  }

  // ── Derived rows ───────────────────────────────────────────────────────────
  const sortedRows = useMemo(() => {
    return [...melodies].sort((a, b) => {
      const av = getSortValue(a, sortKey)
      const bv = getSortValue(b, sortKey)
      if (typeof av === 'string' && typeof bv === 'string') {
        return sortDir === 'asc' ? av.localeCompare(bv) : bv.localeCompare(av)
      }
      return sortDir === 'asc' ? av - bv : bv - av
    })
  }, [melodies, sortKey, sortDir, displayLang]) // eslint-disable-line react-hooks/exhaustive-deps

  const total     = sortedRows.length
  const totalPages = Math.max(1, Math.ceil(total / pageSize))
  const safePage   = Math.min(page, totalPages)
  const pagedRows  = sortedRows.slice((safePage - 1) * pageSize, safePage * pageSize)
  const hasFilter  = Boolean(search || typeFilter || toneFilter || statusFilter)

  // ── Actions ────────────────────────────────────────────────────────────────
  const handleDelete = async () => {
    if (!deleteTarget) return
    try {
      await api.delete(`/melodies/${deleteTarget.id}`)
      setDeleteTarget(null)
      fetchMelodies()
      fetchAllCount()
    } catch (err) {
      setError(err.message)
      setDeleteTarget(null)
    }
  }

  const handlePublish = async (row) => {
    setActionLoading(row.id)
    try {
      await api.post(`/melodies/${row.id}/publish`)
      fetchMelodies()
    } catch (err) {
      setError(err.message)
    } finally {
      setActionLoading(null)
    }
  }

  const handleUnpublish = async () => {
    if (!unpublishTarget) return
    setActionLoading(unpublishTarget.id)
    try {
      await api.post(`/melodies/${unpublishTarget.id}/unpublish`)
      setUnpublishTarget(null)
      fetchMelodies()
    } catch (err) {
      setError(err.message)
      setUnpublishTarget(null)
    } finally {
      setActionLoading(null)
    }
  }

  // ── Bulk actions ───────────────────────────────────────────────────────────
  const handleBulkPublish = async () => {
    setActionsOpen(false)
    setBulkActionLoading(true)
    const ids = [...selectedIds]
    const drafts = melodies.filter((m) => ids.includes(m.id) && m.status !== 'published')
    try {
      await Promise.all(drafts.map((m) => api.post(`/melodies/${m.id}/publish`)))
      fetchMelodies()
    } catch (err) {
      setError(err.message)
    } finally {
      setBulkActionLoading(false)
    }
  }

  const handleBulkUnpublish = async () => {
    setActionsOpen(false)
    setBulkActionLoading(true)
    const ids = [...selectedIds]
    const published = melodies.filter((m) => ids.includes(m.id) && m.status === 'published')
    try {
      await Promise.all(published.map((m) => api.post(`/melodies/${m.id}/unpublish`)))
      fetchMelodies()
    } catch (err) {
      setError(err.message)
    } finally {
      setBulkActionLoading(false)
    }
  }

  const handleBulkDelete = async () => {
    setBulkDeleteConfirm(false)
    setBulkActionLoading(true)
    const ids = [...selectedIds]
    try {
      await Promise.all(ids.map((id) => api.delete(`/melodies/${id}`)))
      exitMultiSelect()
      fetchMelodies()
      fetchAllCount()
    } catch (err) {
      setError(err.message)
    } finally {
      setBulkActionLoading(false)
    }
  }

  const downloadBinary = async (e, row) => {
    if (e?.stopPropagation) e.stopPropagation()
    const token = localStorage.getItem('access_token')
    try {
      const res = await fetch(`/api/melodies/${row.id}/download/binary`, {
        headers: token ? { Authorization: `Bearer ${token}` } : {},
      })
      if (!res.ok) throw new Error(`Download failed: ${res.statusText}`)
      const blob = await res.blob()
      const url  = URL.createObjectURL(blob)
      const a    = document.createElement('a')
      a.href     = url
      a.download = row.pid ? `${row.pid}.bsm` : 'melody.bsm'
      a.click()
      URL.revokeObjectURL(url)
    } catch (err) {
      setError(err.message)
    }
  }

  // ── Cell renderer ──────────────────────────────────────────────────────────
  const renderCell = (key, row) => {
    const info     = row.information   || {}
    const metadata = row.metadata      || {}

    switch (key) {
      case 'color': {
        const hex = normalizeColor(info.color)
        return (
          <div
            style={{
              width: 4,
              height: 30,
              borderRadius: 'var(--radius-sm)',
              backgroundColor: hex || 'var(--color-border-strong)',
              flexShrink: 0,
            }}
            title={info.color || 'No colour'}
          />
        )
      }

      case 'name': {
        const isOutdated = Boolean(info.outdated_archetype)
        const desc = colPrefs.visible.includes('description')
          ? getLocalizedValue(info.description, displayLang)
          : null
        return (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 2, minWidth: 0 }}>
            <span
              style={{
                fontWeight: 'var(--font-weight-medium)',
                color: isOutdated ? 'var(--color-warning)' : 'var(--color-text-primary)',
                whiteSpace: 'nowrap',
              }}
            >
              {getLocalizedValue(info.name, displayLang, 'Untitled')}
            </span>
            {desc && (
              <span
                style={{
                  fontSize: 'var(--font-size-xs)',
                  color: 'var(--color-text-muted)',
                  lineHeight: 'var(--line-height-base)',
                  whiteSpace: 'normal',
                  wordBreak: 'break-word',
                }}
              >
                {desc}
              </span>
            )}
            {info.customTags?.length > 0 && (
              <div style={{ display: 'flex', gap: 'var(--space-1)', flexWrap: 'wrap', marginTop: 2 }}>
                {info.customTags.slice(0, 3).map((tag) => (
                  <span
                    key={tag}
                    style={{
                      fontSize: 'var(--font-size-xs)',
                      padding: '1px 6px',
                      borderRadius: 'var(--radius-full)',
                      backgroundColor: 'var(--color-primary-subtle)',
                      color: 'var(--color-primary)',
                    }}
                  >
                    {tag}
                  </span>
                ))}
              </div>
            )}
          </div>
        )
      }

      case 'status': {
        const isOutdated = Boolean(info.outdated_archetype)
        return (
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 'var(--space-1)' }}>
            <StatusBadge variant={row.status === 'published' ? 'success' : 'neutral'}>
              {row.status === 'published' ? 'Live' : 'Draft'}
            </StatusBadge>
            {isOutdated && (
              <StatusBadge variant="warning" title="Archetype changed — reassign to clear">
                Outdated
              </StatusBadge>
            )}
          </div>
        )
      }

      case 'type': {
        const typeVariant = { orthodox: 'info', catholic: 'danger', all: 'success' }
        if (!row.type) return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
        return (
          <StatusBadge variant={typeVariant[row.type] || 'neutral'}>
            {row.type.charAt(0).toUpperCase() + row.type.slice(1)}
          </StatusBadge>
        )
      }

      case 'tone': {
        if (!info.melodyTone) return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
        return (
          <span style={{ color: 'var(--color-text-secondary)', textTransform: 'capitalize' }}>
            {info.melodyTone}
          </span>
        )
      }

      case 'totalActiveBells':
        return (
          <span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>
            {info.totalActiveBells ?? '—'}
          </span>
        )

      case 'isTrueRing':
        return (
          <StatusBadge variant={info.isTrueRing ? 'success' : 'neutral'}>
            {info.isTrueRing ? 'Yes' : 'No'}
          </StatusBadge>
        )

      case 'minSpeed':
        if (!info.minSpeed) return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
        return (
          <span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', whiteSpace: 'nowrap' }}>
            {formatBpm(info.minSpeed)} <span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>bpm</span>
          </span>
        )

      case 'maxSpeed':
        if (!info.maxSpeed) return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
        return (
          <span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', whiteSpace: 'nowrap' }}>
            {formatBpm(info.maxSpeed)} <span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>bpm</span>
          </span>
        )

      case 'tags':
        if (!info.customTags?.length) return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
        return (
          <div style={{ display: 'flex', gap: 'var(--space-1)', flexWrap: 'wrap' }}>
            {info.customTags.map((tag) => (
              <span
                key={tag}
                style={{
                  fontSize: 'var(--font-size-xs)',
                  padding: '2px 8px',
                  borderRadius: 'var(--radius-full)',
                  backgroundColor: 'var(--color-primary-subtle)',
                  color: 'var(--color-primary)',
                }}
              >
                {tag}
              </span>
            ))}
          </div>
        )

      case 'dateCreated': {
        if (!metadata.dateCreated) return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
        const parts = formatDateShort(metadata.dateCreated)
        return (
          <div style={{ display: 'flex', flexDirection: 'column', lineHeight: 1.35 }}>
            <span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>{parts.date}</span>
            <span style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>{parts.time}</span>
          </div>
        )
      }

      case 'dateEdited':
        return metadata.dateEdited
          ? <span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }} title={fmtDateTimeCentral(metadata.dateEdited)}>{formatRelativeTime(metadata.dateEdited)}</span>
          : <span style={{ color: 'var(--color-text-muted)' }}>—</span>

      case 'createdBy':
        return <span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>{metadata.createdBy || '—'}</span>

      case 'lastEditedBy':
        return <span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>{metadata.lastEditedBy || metadata.editedBy || '—'}</span>

      case 'description': {
        const desc = getLocalizedValue(info.description, displayLang)
        return desc
          ? <span style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)', maxWidth: 240, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', display: 'block' }}>{desc}</span>
          : <span style={{ color: 'var(--color-text-muted)' }}>—</span>
      }

      case 'speed': {
        const ds = row.default_settings || {}
        if (ds.speed == null) return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
        const speedMs = mapPercentageToStepDelay(ds.speed, info.minSpeed, info.maxSpeed)
        const bpm     = formatBpm(speedMs)
        return (
          <div style={{ minWidth: 144 }} title={speedMs ? `${speedMs} ms` : undefined}>
            <div style={{ fontSize: 'var(--font-size-xs)', marginBottom: 4, color: 'var(--color-text-secondary)', whiteSpace: 'nowrap' }}>
              {ds.speed}%{bpm ? ` · ${bpm} bpm` : ''}
            </div>
            <div style={{ width: '100%', height: 6, borderRadius: 'var(--radius-full)', backgroundColor: 'var(--color-border)' }}>
              <div
                style={{
                  height: '100%',
                  borderRadius: 'var(--radius-full)',
                  width: `${Math.max(0, Math.min(100, ds.speed))}%`,
                  backgroundColor: speedBarColor(ds.speed),
                  transition: 'width 300ms ease',
                }}
              />
            </div>
          </div>
        )
      }

      case 'duration': {
        const ds  = row.default_settings || {}
        if (ds.duration == null) return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
        if (Number(ds.duration) === 0) {
          return (
            <span
              style={{
                fontSize: 'var(--font-size-xs)',
                padding: '2px 8px',
                borderRadius: 'var(--radius-full)',
                backgroundColor: 'var(--color-primary-subtle)',
                color: 'var(--color-primary)',
              }}
            >
              Infinite
            </span>
          )
        }
        const allDurations   = melodySettings?.duration_values || []
        const nonZero        = allDurations.filter((v) => Number(v) > 0)
        const idx            = nonZero.indexOf(Number(ds.duration))
        const percent        = idx >= 0 && nonZero.length > 0
          ? Math.round(((idx + 1) / nonZero.length) * 100)
          : 0
        return (
          <div style={{ minWidth: 112 }}>
            <div style={{ fontSize: 'var(--font-size-xs)', marginBottom: 4, color: 'var(--color-text-secondary)', whiteSpace: 'nowrap' }}>
              {formatDurationVerbose(ds.duration)}
            </div>
            <div style={{ width: '100%', height: 6, borderRadius: 'var(--radius-full)', backgroundColor: 'var(--color-border)' }}>
              <div
                style={{
                  height: '100%',
                  borderRadius: 'var(--radius-full)',
                  width: `${Math.max(0, Math.min(100, percent))}%`,
                  backgroundColor: durationBarColor(percent),
                  transition: 'width 300ms ease',
                }}
              />
            </div>
          </div>
        )
      }

      case 'totalRunDuration': {
        const trd = info.totalRunDuration ?? info.runDuration
        if (!trd) return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
        return <span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>{trd} ms</span>
      }

      case 'pauseDuration': {
        const pd = info.pauseDuration
        if (!pd) return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
        return <span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-secondary)' }}>{pd} ms</span>
      }

      case 'infiniteLoop':
        return (
          <StatusBadge variant={info.infiniteLoop ? 'info' : 'neutral'}>
            {info.infiniteLoop ? 'Yes' : 'No'}
          </StatusBadge>
        )

      case 'noteAssignments': {
        const assignments = info.noteAssignments || info.note_assignments
        if (!assignments || !Object.keys(assignments).length)
          return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
        return (
          <span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
            {Object.keys(assignments).length} notes
          </span>
        )
      }

      case 'binaryFile': {
        const hasBinary = row.has_binary_file ?? row.hasBinaryFile ?? false
        return (
          <StatusBadge variant={hasBinary ? 'success' : 'neutral'}>
            {hasBinary ? 'Available' : 'None'}
          </StatusBadge>
        )
      }

      case 'pid':
        return <span style={{ fontFamily: 'var(--font-family-mono)', fontSize: 'var(--font-size-sm)', color: 'var(--color-text-muted)' }}>{row.pid || '—'}</span>

      case 'docId':
        return <DocIdCell id={row.id} />

      default:
        return <span style={{ color: 'var(--color-text-muted)' }}>—</span>
    }
  }

  // ── Build table columns ────────────────────────────────────────────────────
  const checkboxCol = {
    key:   '__checkbox',
    label: '',
    width: '40px',
    render: (row) => (
      <div
        style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}
        onClick={(e) => e.stopPropagation()}
      >
        <input
          type="checkbox"
          checked={selectedIds.has(row.id)}
          onChange={() => toggleSelected(row.id)}
          style={{ accentColor: 'var(--color-primary)', width: 15, height: 15, cursor: 'pointer' }}
        />
      </div>
    ),
  }

  const tableColumns = [
    ...(multiSelectMode ? [checkboxCol] : []),
    ...colPrefs.visible
      .filter((key) => key !== 'description') // rendered inline inside the 'name' cell
      .map((key) => {
        const col = ALL_COLUMNS.find((c) => c.key === key)
        return col ? { ...col, render: (row) => renderCell(key, row) } : null
      }).filter(Boolean),
    ...(!multiSelectMode ? [{
      key:    '__actions',
      label:  '',
      width:  '90px',
      align:  'right',
      render: (row) => (
        <RowActions
          disabled={actionLoading === row.id}
          actions={[
            {
              label:   'View',
              icon:    <Icon name="eye" size={14} />,
              onClick: () => navigate(`/melodies/${row.id}`),
            },
            ...(canEdit ? [
              {
                label: 'Edit',
                icon:  <img src="/src/assets/global-icons/edit.svg" width={14} height={14} alt="" style={{ filter: 'brightness(0) invert(1)', opacity: 0.8 }} />,
                onClick: () => navigate(`/melodies/${row.id}/edit`),
              },
              row.status === 'published'
                ? {
                    label:   'Unpublish',
                    icon:    <Icon name="close" size={14} />,
                    onClick: () => setUnpublishTarget(row),
                    divider: true,
                  }
                : {
                    label:   'Publish',
                    icon:    <Icon name="check" size={14} />,
                    onClick: () => handlePublish(row),
                    divider: true,
                  },
              {
                label:   'Download Binary',
                icon:    <Icon name="download" size={14} />,
                onClick: (e) => downloadBinary(e, row),
              },
              {
                label:   'Delete',
                icon:    <Icon name="delete" size={14} />,
                color:   'var(--color-danger)',
                onClick: () => setDeleteTarget(row),
                divider: true,
              },
            ] : []),
          ]}
        />
      ),
    }] : []),
  ]

  const languages = melodySettings?.available_languages || ['en']

  // ─────────────────────────────────────────────────────────────────────────

  return (
    <div className="page-wrapper">
      <PageHeader
        title="Melody Library"
        subtitle={
          hasFilter
            ? `${sortedRows.length} result${sortedRows.length !== 1 ? 's' : ''} of ${allCount}`
            : `${allCount} melod${allCount !== 1 ? 'ies' : 'y'}`
        }
      >
        <Button variant="secondary" onClick={() => navigate('/melodies/settings')}>
          Settings
        </Button>
        <Button variant="secondary" onClick={() => navigate('/melodies/archetypes')}>
          Archetypes
        </Button>
        {canAdd && (
          <Button variant="primary" onClick={() => navigate('/melodies/new')}>
            <Icon name="add" size={14} />
            Add Melody
          </Button>
        )}
      </PageHeader>

      {/* Filters row */}
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-3)', alignItems: 'center', minWidth: 0 }}>

        <div style={{ flex: '1 1 0', minWidth: '180px' }}>
          <SearchBar
            value={search}
            onChange={setSearch}
            placeholder="Search melodies…"
          />
        </div>

        <Select value={typeFilter} onChange={(e) => setTypeFilter(e.target.value)} placeholder="All Types" style={{ width: '130px', flexShrink: 0 }}>
          <option value="">All Types</option>
          <option value="orthodox">Orthodox</option>
          <option value="catholic">Catholic</option>
          <option value="all">All</option>
        </Select>

        <Select value={toneFilter} onChange={(e) => setToneFilter(e.target.value)} placeholder="All Tones" style={{ width: '145px', flexShrink: 0 }}>
          <option value="">All Tones</option>
          <option value="normal">Normal</option>
          <option value="festive">Festive</option>
          <option value="cheerful">Cheerful</option>
          <option value="lamentation">Lamentation</option>
        </Select>

        <Select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)} placeholder="All Statuses" style={{ width: '145px', flexShrink: 0 }}>
          <option value="">All Statuses</option>
          <option value="published">Published (Live)</option>
          <option value="draft">Drafts</option>
        </Select>

        {languages.length > 1 && (
          <Select value={displayLang} onChange={(e) => setDisplayLang(e.target.value)} style={{ width: '120px', flexShrink: 0 }}>
            {languages.map((l) => (
              <option key={l} value={l}>{LANG_NAMES[l] || l}</option>
            ))}
          </Select>
        )}

        {/* Multi-select / Actions button */}
        {canEdit && (
          <div style={{ position: 'relative', flexShrink: 0 }}>
            {/* Fixed-width wrapper — never resizes; both states share the same box */}
            <div style={{ width: '120px' }}>
              {!multiSelectMode ? (
                <Button
                  variant="secondary"
                  size="md"
                  style={{ width: '100%', justifyContent: 'center', paddingTop: 'var(--space-3)', paddingBottom: 'var(--space-3)' }}
                  onClick={() => setMultiSelectMode(true)}
                >
                  Multi-select
                </Button>
              ) : (
                <Button
                  ref={actionsBtnRef}
                  variant="secondary"
                  size="md"
                  style={{ width: '100%', justifyContent: 'center', paddingTop: 'var(--space-3)', paddingBottom: 'var(--space-3)' }}
                  onClick={() => setActionsOpen((v) => !v)}
                  loading={bulkActionLoading}
                >
                  Actions{selectedIds.size > 0 ? ` (${selectedIds.size})` : ''}
                </Button>
              )}
            </div>

            {actionsOpen && (
              <div
                ref={actionsMenuRef}
                role="menu"
                style={{
                  position: 'absolute',
                  top: 'calc(100% + 4px)',
                  right: 0,
                  zIndex: 200,
                  minWidth: '180px',
                  background: 'var(--color-bg-elevated)',
                  border: '1px solid var(--color-border-strong)',
                  borderRadius: 'var(--radius-md)',
                  boxShadow: 'var(--shadow-lg)',
                  padding: 'var(--space-1) 0',
                  overflow: 'hidden',
                }}
              >
                {[
                  {
                    label: 'Select All',
                    icon: (
                      <svg width="15" height="15" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                        <rect x="1" y="1" width="12" height="12" rx="2" />
                        <path d="M3.5 7l2.5 2.5 4.5-4.5" />
                      </svg>
                    ),
                    onClick: () => {
                      setSelectedIds(new Set(sortedRows.map((r) => r.id)))
                      setActionsOpen(false)
                    },
                  },
                  {
                    label: 'Deselect All',
                    icon: (
                      <svg width="15" height="15" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                        <rect x="1" y="1" width="12" height="12" rx="2" />
                        <path d="M4.5 4.5l5 5M9.5 4.5l-5 5" />
                      </svg>
                    ),
                    onClick: () => {
                      setSelectedIds(new Set())
                      setActionsOpen(false)
                    },
                  },
                  { divider: true },
                  {
                    label: 'Publish',
                    disabled: selectedIds.size === 0,
                    icon: (
                      <svg width="15" height="15" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                        <circle cx="7" cy="7" r="6" />
                        <path d="M4.5 7l2 2 3-3.5" />
                      </svg>
                    ),
                    onClick: handleBulkPublish,
                  },
                  {
                    label: 'Unpublish',
                    disabled: selectedIds.size === 0,
                    icon: (
                      <svg width="15" height="15" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                        <circle cx="7" cy="7" r="6" />
                        <path d="M4.5 7h5" />
                      </svg>
                    ),
                    onClick: handleBulkUnpublish,
                  },
                  { divider: true },
                  {
                    label: 'Delete',
                    danger: true,
                    disabled: selectedIds.size === 0,
                    icon: (
                      <svg width="15" height="15" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                        <path d="M2 3.5h10M5 3.5V2.5a.5.5 0 01.5-.5h3a.5.5 0 01.5.5v1M5.5 6v4.5M8.5 6v4.5M3 3.5l.75 7.5a.5.5 0 00.5.5h5.5a.5.5 0 00.5-.5L11 3.5" />
                      </svg>
                    ),
                    onClick: () => { setActionsOpen(false); setBulkDeleteConfirm(true) },
                  },
                  { divider: true },
                  {
                    label: 'Cancel',
                    icon: (
                      <svg width="15" height="15" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                        <path d="M10.5 3.5l-7 7M3.5 3.5l7 7" />
                      </svg>
                    ),
                    onClick: () => { setActionsOpen(false); exitMultiSelect() },
                  },
                ].map((item, i) => {
                  if (item.divider) {
                    return (
                      <div
                        key={i}
                        style={{
                          height: 1,
                          backgroundColor: 'var(--color-border-strong)',
                          margin: 'var(--space-2) 0',
                        }}
                      />
                    )
                  }
                  return (
                    <button
                      key={item.label}
                      role="menuitem"
                      disabled={item.disabled}
                      onClick={item.onClick}
                      style={{
                        display: 'flex',
                        alignItems: 'center',
                        gap: 'var(--space-3)',
                        width: '100%',
                        padding: 'var(--space-3) var(--space-4)',
                        background: 'transparent',
                        border: 'none',
                        borderRadius: 'var(--radius-md)',
                        fontFamily: 'var(--font-family-base)',
                        fontSize: 'var(--font-size-base)',
                        fontWeight: 'var(--font-weight-medium)',
                        textAlign: 'left',
                        color: item.danger
                          ? 'var(--color-danger)'
                          : item.disabled
                            ? 'var(--color-text-muted)'
                            : 'var(--color-text-secondary)',
                        opacity: item.disabled ? 0.45 : 1,
                        cursor: item.disabled ? 'default' : 'pointer',
                        pointerEvents: item.disabled ? 'none' : undefined,
                        transition: 'background var(--transition-fast)',
                      }}
                      onMouseEnter={(e) => { if (!item.disabled) e.currentTarget.style.background = 'var(--color-bg-island)' }}
                      onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent' }}
                    >
                      {item.icon && (
                        <span style={{ display: 'inline-flex', alignItems: 'center', flexShrink: 0 }}>
                          {item.icon}
                        </span>
                      )}
                      {item.label}
                    </button>
                  )
                })}
              </div>
            )}
          </div>
        )}

        {hasFilter && (
          <Button
            variant="ghost"
            size="sm"
            onClick={() => {
              setSearch('')
              setTypeFilter('')
              setToneFilter('')
              setStatusFilter('')
            }}
          >
            Clear filters
          </Button>
        )}
      </div>

      {/* Error banner */}
      {error && (
        <div
          style={{
            padding: 'var(--space-4)',
            borderRadius: 'var(--radius-lg)',
            backgroundColor: 'var(--color-danger-bg)',
            border: '1px solid var(--color-danger)',
            color: 'var(--color-danger)',
            fontSize: 'var(--font-size-sm)',
          }}
        >
          {error}
        </div>
      )}

      {/* Table */}
      <DataTable
        columns={tableColumns}
        data={pagedRows}
        loading={loading}
        keyField="id"
        emptyMessage="No melodies found"
        emptyDescription={hasFilter ? 'Try adjusting your filters.' : 'Create your first melody to get started.'}
        sortKey={sortKey}
        sortDir={sortDir}
        onSort={handleSort}
        onRowClick={multiSelectMode ? (row) => toggleSelected(row.id) : (row) => navigate(`/melodies/${row.id}`)}
        onRowMiddleClick={multiSelectMode ? undefined : (row) => window.open(`/melodies/${row.id}`, '_blank', 'noopener,noreferrer')}
        selectedIds={selectedIds}
        allColumns={[...ALL_COLUMNS, { key: '__actions', label: '', pickerLabel: 'Actions', alwaysOn: true }]}
        visibleKeys={colPrefs.visible}
        onColumnChange={handleColumnChange}
        footer={
          <Pagination
            page={safePage}
            pageSize={pageSize}
            total={total}
            onPageChange={setPage}
            onSizeChange={(size) => { setPageSize(size); setPage(1) }}
          />
        }
      />

      {/* Delete confirm (single) */}
      <ConfirmDialog
        open={Boolean(deleteTarget)}
        variant="danger"
        title="Delete Melody"
        message={
          deleteTarget
            ? `"${getLocalizedValue(deleteTarget.information?.name, displayLang, 'this melody')}" will be permanently deleted and cannot be recovered.`
            : ''
        }
        confirmLabel="Delete"
        onConfirm={handleDelete}
        onClose={() => setDeleteTarget(null)}
      />

      {/* Unpublish confirm (single) */}
      <ConfirmDialog
        open={Boolean(unpublishTarget)}
        variant="primary"
        title="Unpublish Melody"
        message={
          unpublishTarget
            ? `"${getLocalizedValue(unpublishTarget.information?.name, displayLang, 'this melody')}" will be moved back to draft and removed from live devices.`
            : ''
        }
        confirmLabel="Unpublish"
        onConfirm={handleUnpublish}
        onClose={() => setUnpublishTarget(null)}
      />

      {/* Bulk delete confirm */}
      <ConfirmDialog
        open={bulkDeleteConfirm}
        variant="danger"
        title="Delete Selected Melodies"
        message={`${selectedIds.size} melod${selectedIds.size !== 1 ? 'ies' : 'y'} will be permanently deleted and cannot be recovered.`}
        confirmLabel="Delete All"
        onConfirm={handleBulkDelete}
        onClose={() => setBulkDeleteConfirm(false)}
      />
    </div>
  )
}
