import { useState, useEffect, useRef, useCallback } from 'react'
import Modal from '@/components/ui/Modal'
import Button from '@/components/ui/Button'
import Spinner from '@/components/ui/Spinner'
import api from '@/lib/api'

const NOTE_LABELS = 'ABCDEFGHIJKLMNOP'

// ── Audio helpers ─────────────────────────────────────────────────────────────

function bellFrequency(bellNumber) {
  return 880 * Math.pow(Math.pow(2, 1 / 12), -2 * (bellNumber - 1))
}

function playStep(audioCtx, stepValue, beatDurationMs) {
  if (!stepValue || !audioCtx) return
  const now = audioCtx.currentTime
  const duration = beatDurationMs / 1000
  const fadeIn = 0.005; const fadeOut = 0.03
  for (let bit = 0; bit < 16; bit++) {
    if (stepValue & (1 << bit)) {
      const osc = audioCtx.createOscillator()
      const gain = audioCtx.createGain()
      osc.connect(gain); gain.connect(audioCtx.destination)
      osc.type = 'sine'
      osc.frequency.setValueAtTime(bellFrequency(bit + 1), now)
      gain.gain.setValueAtTime(0, now)
      gain.gain.linearRampToValueAtTime(0.3, now + fadeIn)
      gain.gain.setValueAtTime(0.3, now + Math.max(duration - fadeOut, fadeIn + 0.001))
      gain.gain.linearRampToValueAtTime(0, now + duration)
      osc.start(now); osc.stop(now + duration)
    }
  }
}

function parseBellNotation(notation) {
  const raw = String(notation || '').trim()
  if (raw === '0' || !raw) return 0
  let value = 0
  for (const part of raw.split('+')) {
    const n = parseInt(part.trim(), 10)
    if (!isNaN(n) && n >= 1 && n <= 16) value |= 1 << (n - 1)
  }
  return value
}

function parseStepsString(stepsStr) {
  if (!stepsStr || !String(stepsStr).trim()) return []
  return String(stepsStr).trim().split(',').map((s) => parseBellNotation(s))
}

function normalizePlaybackUrl(url) {
  if (!url || typeof url !== 'string') return null
  if (url.startsWith('http') || url.startsWith('/api')) return url
  if (url.startsWith('/')) return `/api${url}`
  return `/api/${url}`
}

async function decodeBsmBinary(url) {
  const token = localStorage.getItem('access_token')
  const res = await fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
  if (!res.ok) {
    if (url.startsWith('http')) {
      const retry = await fetch(url)
      if (!retry.ok) throw new Error(`Failed to fetch binary: ${retry.statusText}`)
      const buf2 = await retry.arrayBuffer()
      const v2 = new DataView(buf2); const s2 = []
      for (let i = 0; i + 1 < buf2.byteLength; i += 2) s2.push(v2.getUint16(i, false))
      return s2
    }
    throw new Error(`Failed to fetch binary: ${res.statusText}`)
  }
  const buf = await res.arrayBuffer()
  const view = new DataView(buf); const steps = []
  for (let i = 0; i + 1 < buf.byteLength; i += 2) steps.push(view.getUint16(i, false))
  return steps
}

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

function applyNoteAssignments(rawStepValue, noteAssignments) {
  if (!noteAssignments || noteAssignments.length === 0) return rawStepValue
  let result = 0
  for (let bit = 0; bit < 16; bit++) {
    if (rawStepValue & (1 << bit)) {
      const assignedBell = noteAssignments[bit]
      if (assignedBell && assignedBell > 0) result |= 1 << (assignedBell - 1)
    }
  }
  return result
}

function interpolateHue(t) {
  const stops = [[0.0, 190], [0.24, 140], [0.5, 56], [0.82, 30], [1.0, 0]]
  for (let i = 0; i < stops.length - 1; i++) {
    const [aPos, aHue] = stops[i]; const [bPos, bHue] = stops[i + 1]
    if (t >= aPos && t <= bPos) {
      const local = (t - aPos) / (bPos - aPos || 1)
      return aHue + (bHue - aHue) * local
    }
  }
  return stops[stops.length - 1][1]
}

function bellDotColor(bell, palette) {
  const custom = palette?.[bell - 1]
  if (custom) return custom
  if (bell <= 0) return 'rgba(148,163,184,0.7)'
  return `hsl(${interpolateHue(Math.min(1, Math.max(0, (bell - 1) / 15)))}, 78%, 68%)`
}

function bellDotGlow(bell, palette) {
  const custom = palette?.[bell - 1]
  if (custom) return `${custom}66`
  if (bell <= 0) return 'rgba(100,116,139,0.35)'
  return `hsla(${interpolateHue(Math.min(1, Math.max(0, (bell - 1) / 15)))}, 78%, 56%, 0.45)`
}

// ── Component ─────────────────────────────────────────────────────────────────

export default function PlaybackModal({ open, onClose, melody, builtMelody, files, archetypeCsv }) {
  const info = melody?.information || {}
  const minSpeed = info.minSpeed || null
  const maxSpeed = info.maxSpeed || null
  const noteAssignments = melody?.default_settings?.noteAssignments || []

  const [steps, setSteps] = useState([])
  const [loading, setLoading] = useState(false)
  const [loadError, setLoadError] = useState('')
  const [playing, setPlaying] = useState(false)
  const [currentStep, setCurrentStep] = useState(-1)
  const [speedPercent, setSpeedPercent] = useState(50)
  const [toneLengthMs, setToneLengthMs] = useState(80)
  const [loopEnabled, setLoopEnabled] = useState(true)
  const [activeBells, setActiveBells] = useState(new Set())
  const [noteColors, setNoteColors] = useState([])

  const audioCtxRef = useRef(null)
  const playbackRef = useRef(null)
  const stepsRef = useRef([])
  const speedMsRef = useRef(500)
  const toneLengthRef = useRef(80)
  const noteAssignmentsRef = useRef(noteAssignments)
  const loopEnabledRef = useRef(true)

  const speedMs = mapPercentageToSpeed(speedPercent, minSpeed, maxSpeed) ?? 500

  useEffect(() => { stepsRef.current = steps }, [steps])
  useEffect(() => { speedMsRef.current = speedMs }, [speedMs])
  useEffect(() => { toneLengthRef.current = toneLengthMs }, [toneLengthMs])
  useEffect(() => { noteAssignmentsRef.current = noteAssignments }, [noteAssignments])
  useEffect(() => { loopEnabledRef.current = loopEnabled }, [loopEnabled])

  useEffect(() => {
    if (!open) return
    let canceled = false
    api.get('/settings/melody')
      .then((s) => { if (!canceled) setNoteColors((s?.note_assignment_colors || []).slice(0, 16)) })
      .catch(() => { if (!canceled) setNoteColors([]) })
    return () => { canceled = true }
  }, [open])

  const stopPlayback = useCallback(() => {
    if (playbackRef.current) {
      clearTimeout(playbackRef.current.timer)
      if (playbackRef.current.flashTimer) clearTimeout(playbackRef.current.flashTimer)
      playbackRef.current = null
    }
    setPlaying(false); setCurrentStep(-1); setActiveBells(new Set())
  }, [])

  useEffect(() => {
    if (!open) {
      stopPlayback()
      setSteps([]); setCurrentStep(-1); setLoadError('')
      setSpeedPercent(50); setLoopEnabled(true); setActiveBells(new Set())
      return
    }
    const binaryUrlCandidate = builtMelody?.binary_url
      ? `/api${builtMelody.binary_url}`
      : files?.binary_url || melody?.url || null
    const binaryUrl = normalizePlaybackUrl(binaryUrlCandidate)
    const csv = archetypeCsv || info.archetype_csv || null

    if (binaryUrl) {
      setLoading(true); setLoadError('')
      decodeBsmBinary(binaryUrl)
        .then((decoded) => { setSteps(decoded); stepsRef.current = decoded })
        .catch((err) => {
          if (csv) { const p = parseStepsString(csv); setSteps(p); stepsRef.current = p; return }
          setLoadError(err.message || 'Failed to load melody data.')
        })
        .finally(() => setLoading(false))
      return
    }
    if (csv) { const p = parseStepsString(csv); setSteps(p); stepsRef.current = p; return }
    setLoadError('No binary or archetype data available for this melody.')
  }, [open]) // eslint-disable-line react-hooks/exhaustive-deps

  const ensureAudioCtx = () => {
    if (!audioCtxRef.current || audioCtxRef.current.state === 'closed')
      audioCtxRef.current = new (window.AudioContext || window.webkitAudioContext)()
    if (audioCtxRef.current.state === 'suspended') audioCtxRef.current.resume()
    return audioCtxRef.current
  }

  const scheduleStep = useCallback((stepIndex) => {
    const currentSteps = stepsRef.current
    if (!currentSteps.length) return
    const playFrom = stepIndex % currentSteps.length
    const ctx = ensureAudioCtx()
    const rawStepValue = currentSteps[playFrom]
    const stepValue = applyNoteAssignments(rawStepValue, noteAssignmentsRef.current)
    setCurrentStep(playFrom)
    const bellsNow = new Set()
    for (let bit = 0; bit < 16; bit++) { if (stepValue & (1 << bit)) bellsNow.add(bit + 1) }
    setActiveBells(bellsNow)
    playStep(ctx, stepValue, toneLengthRef.current)
    const flashTimer = setTimeout(() => setActiveBells(new Set()), toneLengthRef.current)
    const timer = setTimeout(() => {
      const next = playFrom + 1
      if (next >= stepsRef.current.length) {
        if (loopEnabledRef.current) scheduleStep(0)
        else stopPlayback()
        return
      }
      scheduleStep(next)
    }, speedMsRef.current)
    playbackRef.current = { timer, flashTimer, stepIndex: playFrom }
  }, [stopPlayback])

  const handlePlay = () => {
    if (!stepsRef.current.length) return
    setPlaying(true); scheduleStep(0)
  }

  const handleClose = () => { stopPlayback(); onClose() }

  const totalSteps = steps.length
  const allBellsUsed = steps.reduce((set, v) => {
    const mapped = applyNoteAssignments(v, noteAssignments)
    for (let bit = 0; bit < 16; bit++) { if (mapped & (1 << bit)) set.add(bit + 1) }
    return set
  }, new Set())
  const maxBell = allBellsUsed.size > 0 ? Math.max(...Array.from(allBellsUsed)) : 0
  const hasSpeedInfo = minSpeed != null && maxSpeed != null
  const configuredNoteCount = Number(info.totalNotes || noteAssignments.length || 0)
  const detectedNoteCount = steps.reduce((max, sv) => {
    let h = 0
    for (let b = 15; b >= 0; b--) { if (sv & (1 << b)) { h = b + 1; break } }
    return Math.max(max, h)
  }, 0)
  const gridNoteCount = Math.max(1, Math.min(16, configuredNoteCount || detectedNoteCount || 1))

  return (
    <Modal
      open={open}
      onClose={handleClose}
      title="Melody Playback"
      size="lg"
      footer={<Button variant="ghost" onClick={handleClose}>Close</Button>}
    >
      {loading && (
        <div style={{ display: 'flex', justifyContent: 'center', padding: 'var(--space-10)' }}>
          <Spinner size="md" />
        </div>
      )}

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

      {!loading && !loadError && totalSteps > 0 && (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-5)' }}>
          <div className="grid grid-cols-1 lg:grid-cols-2 gap-5">

            {/* Left: note assignments + bell indicators */}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-4)' }}>
              <p style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)' }}>
                {totalSteps} steps · {allBellsUsed.size} bell{allBellsUsed.size !== 1 ? 's' : ''}
              </p>

              {noteAssignments.length > 0 && (
                <div>
                  <p style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', marginBottom: 'var(--space-2)' }}>Note → Bell Assignment</p>
                  <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-1)' }}>
                    {noteAssignments.map((assignedBell, noteIdx) => {
                      const fires = assignedBell && assignedBell > 0
                      const isActive = fires && activeBells.has(assignedBell)
                      return (
                        <div key={noteIdx} style={{
                          minWidth: 36, padding: '4px 6px', borderRadius: 'var(--radius-sm)',
                          border: `1px solid ${isActive ? 'var(--color-primary)' : 'var(--color-border)'}`,
                          backgroundColor: isActive ? 'var(--color-primary)' : 'var(--color-bg-elevated)',
                          display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 2,
                          transition: 'background-color 80ms ease',
                        }}>
                          <span style={{ fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-bold)', color: isActive ? '#fff' : 'var(--color-text-secondary)' }}>
                            {NOTE_LABELS[noteIdx]}
                          </span>
                          <div style={{ width: '100%', height: 1, backgroundColor: isActive ? 'rgba(255,255,255,0.4)' : 'var(--color-border)' }} />
                          <span style={{ fontSize: 'var(--font-size-xs)', color: isActive ? '#fff' : 'var(--color-text-muted)' }}>
                            {assignedBell > 0 ? assignedBell : '–'}
                          </span>
                        </div>
                      )
                    })}
                  </div>
                </div>
              )}

              {maxBell > 0 && (
                <div>
                  <p style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', marginBottom: 'var(--space-2)' }}>Active Bells</p>
                  <div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--space-1)' }}>
                    {Array.from({ length: maxBell }, (_, i) => i + 1).map((b) => {
                      const isActive = activeBells.has(b)
                      const isUsed = allBellsUsed.has(b)
                      return (
                        <div key={b} style={{
                          width: 32, height: 32, borderRadius: '9999px',
                          display: 'flex', alignItems: 'center', justifyContent: 'center',
                          fontSize: 'var(--font-size-xs)', fontWeight: 'var(--font-weight-bold)',
                          backgroundColor: isActive ? '#22c55e' : isUsed ? 'var(--color-bg-elevated)' : 'var(--color-bg-page)',
                          color: isActive ? '#fff' : isUsed ? 'var(--color-text-secondary)' : 'var(--color-border)',
                          border: `2px solid ${isActive ? '#22c55e' : 'var(--color-border)'}`,
                          transition: 'background-color 80ms ease',
                        }}>
                          {b}
                        </div>
                      )
                    })}
                  </div>
                </div>
              )}
            </div>

            {/* Right: playback controls + sliders */}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--space-4)' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 'var(--space-3)' }}>
                {!playing ? (
                  <Button variant="primary" onClick={handlePlay}>Play</Button>
                ) : (
                  <Button variant="danger" onClick={stopPlayback}>Stop</Button>
                )}
                <label style={{ display: 'inline-flex', alignItems: 'center', gap: 'var(--space-2)', fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', cursor: 'pointer' }}>
                  <input type="checkbox" checked={loopEnabled} onChange={(e) => setLoopEnabled(e.target.checked)} style={{ width: 14, height: 14 }} />
                  Loop
                </label>
                {currentStep >= 0 && (
                  <div style={{ marginLeft: 'auto' }}>
                    <span style={{ fontSize: 'var(--font-size-xs)', fontFamily: 'var(--font-family-mono)', color: 'var(--color-primary)' }}>
                      Step {currentStep + 1} / {totalSteps}
                    </span>
                    <div style={{ marginTop: 4, height: 6, borderRadius: 'var(--radius-full)', backgroundColor: 'var(--color-bg-elevated)', border: '1px solid var(--color-border)', overflow: 'hidden' }}>
                      <div style={{ height: '100%', backgroundColor: 'var(--color-primary)', width: `${((currentStep + 1) / totalSteps) * 100}%`, transition: 'width 50ms linear' }} />
                    </div>
                  </div>
                )}
              </div>

              <div>
                <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 'var(--space-1)' }}>
                  <span style={{ fontSize: 'var(--font-size-sm)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-secondary)' }}>Speed</span>
                  <span style={{ fontSize: 'var(--font-size-sm)', fontWeight: 'var(--font-weight-bold)', color: 'var(--color-primary)' }}>
                    {speedPercent}%{hasSpeedInfo ? ` · ${speedMs} ms` : ''}
                  </span>
                </div>
                <input type="range" min="1" max="100" step="1" value={speedPercent}
                  onChange={(e) => setSpeedPercent(Number(e.target.value))}
                  style={{ width: '100%', accentColor: 'var(--color-primary)', cursor: 'pointer' }}
                />
              </div>

              <div>
                <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 'var(--space-1)' }}>
                  <span style={{ fontSize: 'var(--font-size-sm)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-secondary)' }}>Tone Length</span>
                  <span style={{ fontSize: 'var(--font-size-sm)', fontWeight: 'var(--font-weight-bold)', color: 'var(--color-primary)' }}>{toneLengthMs} ms</span>
                </div>
                <input type="range" min="20" max="400" step="10" value={toneLengthMs}
                  onChange={(e) => setToneLengthMs(Number(e.target.value))}
                  style={{ width: '100%', accentColor: 'var(--color-primary)', cursor: 'pointer' }}
                />
              </div>
            </div>
          </div>

          {/* Step matrix */}
          <div>
            <p style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-text-muted)', marginBottom: 'var(--space-2)' }}>Note / Step Matrix</p>
            <div style={{ overflowX: 'auto', maxHeight: 340, overflowY: 'auto', border: '1px solid var(--color-border)', borderRadius: 'var(--radius-md)' }}>
              <table style={{ borderCollapse: 'separate', borderSpacing: 0, fontSize: 'var(--font-size-xs)', minWidth: 'max-content' }}>
                <thead>
                  <tr>
                    <th style={{ position: 'sticky', top: 0, left: 0, zIndex: 20, padding: '6px 8px', textAlign: 'left', borderBottom: '1px solid var(--color-border)', borderRight: '1px solid var(--color-border)', backgroundColor: 'var(--color-bg-page)', color: 'var(--color-text-muted)' }}>
                      Note \ Step
                    </th>
                    {steps.map((_, si) => (
                      <th key={si} style={{
                        position: 'sticky', top: 0, zIndex: 10, padding: '6px 8px', textAlign: 'center', minWidth: 40,
                        borderBottom: '1px solid var(--color-border)', borderRight: '1px solid var(--color-border)',
                        backgroundColor: currentStep === si ? 'rgba(var(--color-primary-rgb, 88,101,242),0.15)' : 'var(--color-bg-page)',
                        color: currentStep === si ? 'var(--color-primary)' : 'var(--color-text-muted)',
                      }}>
                        {si + 1}
                      </th>
                    ))}
                  </tr>
                </thead>
                <tbody>
                  {Array.from({ length: gridNoteCount }, (_, noteIdx) => (
                    <tr key={noteIdx}>
                      <th style={{ position: 'sticky', left: 0, zIndex: 1, padding: '6px 8px', textAlign: 'left', borderBottom: '1px solid var(--color-border)', borderRight: '1px solid var(--color-border)', backgroundColor: 'var(--color-bg-page)', color: 'var(--color-text-secondary)' }}>
                        {NOTE_LABELS[noteIdx]}
                      </th>
                      {steps.map((sv, si) => {
                        const enabled = Boolean(sv & (1 << noteIdx))
                        const assignedBell = Number(noteAssignments[noteIdx] || 0)
                        const isCurrent = currentStep === si
                        return (
                          <td key={`${noteIdx}-${si}`} style={{
                            width: 40, height: 40,
                            borderBottom: '1px solid var(--color-border)', borderRight: '1px solid var(--color-border)',
                            backgroundColor: isCurrent ? 'rgba(var(--color-primary-rgb, 88,101,242),0.05)' : 'transparent',
                          }}>
                            <span style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                              <span style={{
                                width: '64%', height: '64%', borderRadius: '9999px', display: 'block',
                                backgroundColor: assignedBell > 0 ? bellDotColor(assignedBell, noteColors) : 'rgba(148,163,184,0.7)',
                                opacity: enabled ? 1 : 0,
                                transform: enabled ? 'scale(1)' : 'scale(0.4)',
                                boxShadow: enabled ? `0 0 10px 3px ${bellDotGlow(assignedBell > 0 ? assignedBell : noteIdx + 1, noteColors)}` : 'none',
                                transition: 'opacity 140ms ease, transform 140ms ease',
                              }} />
                            </span>
                          </td>
                        )
                      })}
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>

          {!hasSpeedInfo && (
            <p style={{ fontSize: 'var(--font-size-xs)', color: 'var(--color-warning, #f59e0b)' }}>
              No MIN/MAX speed set — using linear fallback.
            </p>
          )}
        </div>
      )}
    </Modal>
  )
}
