import { useState, useEffect, useRef, useCallback } from "react";

// ============================================================================
// Web Audio Engine
// ============================================================================

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 freq = bellFrequency(bit + 1);
      const osc = audioCtx.createOscillator();
      const gain = audioCtx.createGain();
      osc.connect(gain);
      gain.connect(audioCtx.destination);
      osc.type = "sine";
      osc.frequency.setValueAtTime(freq, 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) {
  notation = notation.trim();
  if (notation === "0" || !notation) return 0;
  let value = 0;
  for (const part of notation.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 || !stepsStr.trim()) return [];
  return stepsStr.trim().split(",").map((s) => parseBellNotation(s));
}

async function decodeBsmBinary(url) {
  // Try with auth token first (for our API endpoints), then without (for Firebase URLs)
  const token = localStorage.getItem("access_token");
  let res = null;

  try {
    res = await fetch(url, {
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    });
  } catch {
    throw new Error("Failed to fetch binary: network error");
  }

  // If unauthorized and it looks like a Firebase URL, try without auth header
  if (!res.ok && res.status === 401 && url.startsWith("http")) {
    try {
      res = await fetch(url);
    } catch {
      throw new Error("Failed to fetch binary: network error");
    }
  }

  if (!res.ok) 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;
}

// ============================================================================
// Speed math — exponential mapping
// ============================================================================

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));
}

// ============================================================================
// Apply note assignments: map archetype note bits → assigned bell bits
//
// The archetype steps encode which NOTES fire using bit flags (note 1 = bit 0,
// note 2 = bit 1, etc). noteAssignments[noteIdx] gives the bell number to fire
// for that note (0 = silence / no bell). We rebuild the step value using the
// assigned bells instead of the raw note numbers.
// ============================================================================

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 noteIdx = bit; // bit 0 = note 1, bit 1 = note 2, ...
      const assignedBell = noteAssignments[noteIdx];
      if (assignedBell && assignedBell > 0) {
        result |= 1 << (assignedBell - 1);
      }
      // assignedBell === 0 means silence — do not set any bell bit
    }
  }
  return result;
}

// ============================================================================
// Component
// ============================================================================

const mutedStyle = { color: "var(--text-muted)" };
const labelStyle = { color: "var(--text-secondary)" };

const NOTE_LABELS = "ABCDEFGHIJKLMNOP";

export default function PlaybackModal({ open, melody, builtMelody, files, archetypeCsv, onClose }) {
  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);

  // activeBells: Set of bell numbers currently lit (for flash effect)
  const [activeBells, setActiveBells] = useState(new Set());

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

  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]); // eslint-disable-line react-hooks/exhaustive-deps

  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());
  }, []);

  // Load steps on open
  useEffect(() => {
    if (!open) {
      stopPlayback();
      setSteps([]);
      setCurrentStep(-1);
      setLoadError("");
      setSpeedPercent(50);
      setActiveBells(new Set());
      return;
    }

    const csv = archetypeCsv || info.archetype_csv || null;
    if (csv) {
      const parsed = parseStepsString(csv);
      setSteps(parsed);
      stepsRef.current = parsed;
      setLoadError("");
      return;
    }

    // Fall back to binary
    const binaryUrl = builtMelody?.binary_url
      ? `/api${builtMelody.binary_url}`
      : files?.binary_url || melody?.url || null;

    if (!binaryUrl) {
      setLoadError("No binary or archetype data available for this melody.");
      return;
    }

    setLoading(true);
    setLoadError("");
    decodeBsmBinary(binaryUrl)
      .then((decoded) => {
        setSteps(decoded);
        stepsRef.current = decoded;
      })
      .catch((err) => setLoadError(err.message))
      .finally(() => setLoading(false));
  }, [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];

    // Map archetype notes → assigned bells
    const stepValue = applyNoteAssignments(rawStepValue, noteAssignmentsRef.current);

    setCurrentStep(playFrom);

    // Flash active bells for tone length, then clear
    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);

    // Clear bell highlight after tone length
    const flashTimer = setTimeout(() => {
      setActiveBells(new Set());
    }, toneLengthRef.current);

    // Schedule next step after step interval
    const timer = setTimeout(() => {
      const next = playFrom + 1;
      scheduleStep(next >= stepsRef.current.length ? 0 : next);
    }, speedMsRef.current);

    playbackRef.current = { timer, flashTimer, stepIndex: playFrom };
  }, []); // eslint-disable-line react-hooks/exhaustive-deps

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

  const handleStop = () => {
    stopPlayback();
  };

  if (!open) return null;

  const totalSteps = steps.length;

  // Compute which bells are actually used (after assignment mapping)
  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;

  return (
    <div
      className="fixed inset-0 flex items-center justify-center z-50"
      style={{ backgroundColor: "rgba(0,0,0,0.7)" }}
      onClick={(e) => e.target === e.currentTarget && !playing && onClose()}
    >
      <div
        className="w-full rounded-lg border shadow-xl"
        style={{
          backgroundColor: "var(--bg-card)",
          borderColor: "var(--border-primary)",
          maxWidth: "480px",
        }}
      >
        {/* Header */}
        <div
          className="flex items-center justify-between px-6 py-4 border-b"
          style={{ borderColor: "var(--border-primary)" }}
        >
          <div>
            <h2 className="text-lg font-semibold" style={{ color: "var(--text-heading)" }}>
              Melody Playback
            </h2>
            <p className="text-xs mt-0.5" style={mutedStyle}>
              {melody?.information?.name?.en || "Melody"} — looping
            </p>
          </div>
          <button
            onClick={() => { stopPlayback(); onClose(); }}
            className="text-xl leading-none"
            style={mutedStyle}
          >
            &times;
          </button>
        </div>

        {/* Body */}
        <div className="px-6 py-5 space-y-5">
          {loading && (
            <p className="text-sm text-center py-4" style={mutedStyle}>Loading binary...</p>
          )}
          {loadError && (
            <div
              className="text-sm rounded-md p-3 border"
              style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}
            >
              {loadError}
            </div>
          )}

          {!loading && !loadError && totalSteps > 0 && (
            <>
              {/* Step info */}
              <div className="flex items-center justify-between">
                <span className="text-xs" style={mutedStyle}>
                  {totalSteps} steps &nbsp;·&nbsp; {allBellsUsed.size} bell{allBellsUsed.size !== 1 ? "s" : ""}
                </span>
                {currentStep >= 0 && (
                  <span className="text-xs font-mono" style={{ color: "var(--accent)" }}>
                    Step {currentStep + 1} / {totalSteps}
                  </span>
                )}
              </div>

              {/* Note → Bell assignment visualizer (shows when assignments exist) */}
              {noteAssignments.length > 0 ? (
                <div>
                  <p className="text-xs mb-2" style={mutedStyle}>Note → Assigned Bell</p>
                  <div className="flex flex-wrap gap-1.5">
                    {noteAssignments.map((assignedBell, noteIdx) => {
                      // A note is active (flashing) if its assigned bell is currently lit in activeBells
                      const firesABell = assignedBell && assignedBell > 0;
                      const isActive = firesABell && activeBells.has(assignedBell);
                      return (
                        <div
                          key={noteIdx}
                          className="flex flex-col items-center rounded-md border transition-all"
                          style={{
                            minWidth: "36px",
                            padding: "4px 6px",
                            backgroundColor: isActive && firesABell
                              ? "var(--accent)"
                              : isActive && !firesABell
                              ? "rgba(156,163,175,0.15)"
                              : "var(--bg-card-hover)",
                            borderColor: isActive ? "var(--accent)" : "var(--border-primary)",
                            transform: isActive && firesABell ? "scale(1.1)" : "scale(1)",
                            opacity: isActive && !firesABell ? 0.5 : 1,
                          }}
                        >
                          <span className="text-xs font-bold leading-tight" style={{ color: isActive && firesABell ? "var(--bg-primary)" : "var(--text-secondary)" }}>
                            {NOTE_LABELS[noteIdx]}
                          </span>
                          <div className="w-full my-0.5" style={{ height: "1px", backgroundColor: isActive && firesABell ? "rgba(255,255,255,0.4)" : "var(--border-primary)" }} />
                          <span className="text-xs leading-tight" style={{ color: isActive && firesABell ? "var(--bg-primary)" : "var(--text-muted)" }}>
                            {assignedBell > 0 ? assignedBell : "—"}
                          </span>
                        </div>
                      );
                    })}
                  </div>
                  <p className="text-xs mt-1" style={mutedStyle}>Top = Note, Bottom = Bell assigned</p>
                </div>
              ) : null}

              {/* Active Bell circles (always shown) */}
              {maxBell > 0 && (
                <div>
                  <p className="text-xs mb-2" style={mutedStyle}>Active Bells</p>
                  <div className="flex flex-wrap gap-1.5">
                    {Array.from({ length: maxBell }, (_, i) => i + 1).map((b) => {
                      const isActive = activeBells.has(b);
                      const isUsed = allBellsUsed.has(b);
                      return (
                        <div
                          key={b}
                          className="w-8 h-8 rounded-full flex items-center justify-center text-xs font-bold"
                          style={{
                            backgroundColor: isActive ? "#22c55e" : isUsed ? "var(--bg-card-hover)" : "var(--bg-primary)",
                            color: isActive ? "#fff" : isUsed ? "var(--text-secondary)" : "var(--border-primary)",
                            border: `2px solid ${isActive ? "#22c55e" : "var(--border-primary)"}`,
                            transition: "background-color 0.05s, border-color 0.05s",
                            transform: isActive ? "scale(1.15)" : "scale(1)",
                          }}
                        >
                          {b}
                        </div>
                      );
                    })}
                  </div>
                </div>
              )}

              {/* Play / Stop */}
              <div className="flex items-center gap-3">
                {!playing ? (
                  <button
                    onClick={handlePlay}
                    className="px-5 py-2 text-sm rounded-md font-medium transition-colors"
                    style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
                  >
                    Play
                  </button>
                ) : (
                  <button
                    onClick={handleStop}
                    className="px-5 py-2 text-sm rounded-md font-medium transition-colors"
                    style={{ backgroundColor: "var(--danger-btn)", color: "var(--text-white)" }}
                  >
                    Stop
                  </button>
                )}
                <span className="text-xs" style={mutedStyle}>Loops continuously</span>
              </div>

              {/* Speed Slider */}
              <div>
                <div className="flex items-center justify-between mb-1.5">
                  <label className="text-sm font-medium" style={labelStyle}>Speed</label>
                  <div className="text-right">
                    <span className="text-sm font-bold" style={{ color: "var(--accent)" }}>
                      {speedPercent}%
                    </span>
                    {hasSpeedInfo && (
                      <span className="text-xs ml-2" style={mutedStyle}>
                        ({speedMs} ms/step)
                      </span>
                    )}
                  </div>
                </div>
                <input
                  type="range"
                  min="1"
                  max="100"
                  step="1"
                  value={speedPercent}
                  onChange={(e) => setSpeedPercent(Number(e.target.value))}
                  className="w-full h-2 rounded-lg appearance-none cursor-pointer"
                />
                <div className="flex justify-between text-xs mt-0.5" style={mutedStyle}>
                  <span>1% (slowest)</span>
                  <span>100% (fastest)</span>
                </div>
                {!hasSpeedInfo && (
                  <p className="text-xs mt-1.5" style={{ color: "var(--warning, #f59e0b)" }}>
                    No MIN/MAX speed set for this melody — using linear fallback.
                  </p>
                )}
              </div>

              {/* Tone Length Slider */}
              <div>
                <div className="flex items-center justify-between mb-1.5">
                  <label className="text-sm font-medium" style={labelStyle}>Tone Length</label>
                  <span className="text-sm font-bold" style={{ color: "var(--accent)" }}>
                    {toneLengthMs} ms
                  </span>
                </div>
                <input
                  type="range"
                  min="20"
                  max="400"
                  step="10"
                  value={toneLengthMs}
                  onChange={(e) => setToneLengthMs(Number(e.target.value))}
                  className="w-full h-2 rounded-lg appearance-none cursor-pointer"
                />
                <div className="flex justify-between text-xs mt-0.5" style={mutedStyle}>
                  <span>Short (20 ms)</span>
                  <span>Long (400 ms)</span>
                </div>
              </div>
            </>
          )}
        </div>

        {/* Footer */}
        <div
          className="flex justify-end px-6 py-4 border-t"
          style={{ borderColor: "var(--border-primary)" }}
        >
          <button
            onClick={() => { stopPlayback(); onClose(); }}
            className="px-4 py-2 text-sm rounded-md transition-colors"
            style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-primary)" }}
          >
            Close
          </button>
        </div>
      </div>
    </div>
  );
}
