/* store.jsx — shared QA results store + run logic */

const QACtx = React.createContext(null);
const useQA = () => React.useContext(QACtx);
const LS_RESULTS = "rlwc_qa_results_v3";
const LS_SESSIONS = "rlwc_qa_sessions_v3";
const LS_VIEW_DATE = "rlwc_qa_view_date_v3";
const LS_OPERATOR = "rlwc_qa_operator_v3";
const LS_DEVICE_ID = "rlwc_qa_device_id_v3";
const LS_INPUT_DEVICE_ID = "rlwc_qa_input_device_id_v4";
const LS_OUTPUT_DEVICE_ID = "rlwc_qa_output_device_id_v4";

function runKey(item, ctx) { return ctx ? `${item.id}::${ctx}` : item.id; }

function sundayLabel(date) {
  return window.fmtDate(date, { month: "short", day: "numeric" });
}

function todayIso() {
  return new Date().toISOString().slice(0, 10);
}

function nextSundayIso(fromIso) {
  const d = new Date((fromIso || todayIso()) + "T12:00:00");
  const add = (7 - d.getDay()) || 7;
  d.setDate(d.getDate() + add);
  return d.toISOString().slice(0, 10);
}

function makeSession(date, operator) {
  return {
    date,
    label: sundayLabel(date),
    name: "Sunday Service",
    operator: operator || "",
    duration: "00:00:00",
    createdAt: new Date().toISOString(),
  };
}

function loadSessions() {
  try {
    const saved = JSON.parse(localStorage.getItem(LS_SESSIONS));
    if (Array.isArray(saved) && saved.length) {
      return saved.sort((a, b) => a.date.localeCompare(b.date));
    }
  } catch (e) {}
  return [makeSession(window.QA_DATA.session.date || todayIso(), "")];
}

function mapAudioDevices(list) {
  return list
    .filter(d => d.kind === "audiooutput" || d.kind === "audioinput")
    .map((d, i) => {
      const hidden = !d.label;
      const label = hidden
        ? `${d.kind === "audiooutput" ? "Output" : "Input"} ${i + 1} - name hidden`
        : d.label.replace(/^Default\s*-\s*/i, "");
      return {
        id: d.deviceId || `${d.kind}-${i}`,
        groupId: d.groupId || "",
        kind: d.kind,
        label,
        driver: d.kind === "audiooutput" ? "System output" : "System input",
        real: true,
        recommended: /macbook pro (microphone|speakers)/i.test(label),
      };
    });
}

function preferredInput(inputs, currentId) {
  const current = inputs.find(d => d.id === currentId);
  if (current) return current;
  return inputs.find(d => /macbook pro microphone/i.test(d.label))
    || inputs.find(d => !/blackhole|teams|zoom|virtual/i.test(d.label))
    || inputs[0]
    || null;
}

function preferredOutput(outputs, currentId) {
  const current = outputs.find(d => d.id === currentId);
  if (current) return current;
  return outputs.find(d => /macbook pro speakers/i.test(d.label)) || outputs[0] || null;
}

function QAProvider({ children }) {
  const [sessions, setSessions] = React.useState(loadSessions);
  const [viewDate, setViewDateState] = React.useState(() => {
    try { return localStorage.getItem(LS_VIEW_DATE) || sessions[sessions.length - 1].date; } catch (e) { return sessions[sessions.length - 1].date; }
  });
  const [resultsBySession, setResultsBySession] = React.useState(() => {
    try { return JSON.parse(localStorage.getItem(LS_RESULTS)) || {}; } catch (e) { return {}; }
  });
  const [operator, setOperatorState] = React.useState(() => {
    try { return localStorage.getItem(LS_OPERATOR) || ""; } catch (e) { return ""; }
  });
  const [inputDeviceId, setInputDeviceIdState] = React.useState(() => {
    try { return localStorage.getItem(LS_INPUT_DEVICE_ID) || localStorage.getItem(LS_DEVICE_ID) || ""; } catch (e) { return ""; }
  });
  const [outputDeviceId, setOutputDeviceIdState] = React.useState(() => {
    try { return localStorage.getItem(LS_OUTPUT_DEVICE_ID) || ""; } catch (e) { return ""; }
  });
  const [audioDevices, setAudioDevices] = React.useState([]);
  const [deviceAccess, setDeviceAccess] = React.useState("idle");
  const [deviceNote, setDeviceNote] = React.useState("");

  React.useEffect(() => { try { localStorage.setItem(LS_SESSIONS, JSON.stringify(sessions)); } catch (e) {} }, [sessions]);
  React.useEffect(() => { try { localStorage.setItem(LS_RESULTS, JSON.stringify(resultsBySession)); } catch (e) {} }, [resultsBySession]);
  React.useEffect(() => { try { localStorage.setItem(LS_VIEW_DATE, viewDate); } catch (e) {} }, [viewDate]);
  React.useEffect(() => { try { localStorage.setItem(LS_OPERATOR, operator); } catch (e) {} }, [operator]);
  React.useEffect(() => { try { localStorage.setItem(LS_INPUT_DEVICE_ID, inputDeviceId); } catch (e) {} }, [inputDeviceId]);
  React.useEffect(() => { try { localStorage.setItem(LS_OUTPUT_DEVICE_ID, outputDeviceId); } catch (e) {} }, [outputDeviceId]);

  function setViewDate(date) {
    stop();
    setViewDateState(date);
  }

  const currentSession = sessions.find(s => s.date === viewDate) || sessions[sessions.length - 1];
  const operators = React.useMemo(() => {
    const names = new Set();
    sessions.forEach(s => { if (s.operator) names.add(s.operator); });
    Object.values(resultsBySession).forEach(byPerson => Object.keys(byPerson || {}).forEach(n => { if (n) names.add(n); }));
    if (operator) names.add(operator);
    return [...names].sort((a, b) => a.localeCompare(b));
  }, [sessions, resultsBySession, operator]);

  function setOperator(name) {
    const clean = (name || "").trim();
    setOperatorState(clean);
    if (clean) {
      setSessions(prev => prev.map(s => s.date === viewDate && !s.operator ? { ...s, operator: clean } : s));
    }
  }

  function addOperator(name) {
    const clean = (name || "").trim();
    if (!clean) return;
    setOperator(clean);
  }

  function addSunday(date, firstOperator) {
    const cleanDate = date || nextSundayIso(sessions[sessions.length - 1]?.date);
    const cleanOperator = (firstOperator || operator || "").trim();
    setSessions(prev => {
      const exists = prev.some(s => s.date === cleanDate);
      const next = exists ? prev : [...prev, makeSession(cleanDate, cleanOperator)];
      return next.sort((a, b) => a.date.localeCompare(b.date));
    });
    setOperatorState(cleanOperator);
    setViewDateState(cleanDate);
  }

  async function scanDevices(requestAccess) {
    if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {
      setAudioDevices([]);
      setDeviceAccess("unsupported");
      setDeviceNote("This browser does not expose audio devices here.");
      return [];
    }
    setDeviceAccess(requestAccess ? "requesting" : "scanning");
    setDeviceNote("");
    try {
      if (requestAccess) {
        try { await window.AudioEngine.requestAccess(inputDeviceId || null); }
        catch (e) { await window.AudioEngine.requestAccess(null); }
      }
      const mapped = mapAudioDevices(await navigator.mediaDevices.enumerateDevices());
      setAudioDevices(mapped);
      const hasHidden = mapped.some(d => /name hidden/.test(d.label));
      setDeviceAccess(hasHidden ? "needs-permission" : "granted");
      setDeviceNote(hasHidden ? "Allow audio access to show exact device names." : "");
      const inputs = mapped.filter(d => d.kind === "audioinput");
      const outputs = mapped.filter(d => d.kind === "audiooutput");
      if (!hasHidden) {
        const nextInput = preferredInput(inputs, inputDeviceId);
        const nextOutput = preferredOutput(outputs, outputDeviceId);
        if (nextInput) {
          setInputDeviceIdState(nextInput.id);
          if (requestAccess && window.AudioEngine.state.sourceLabel !== nextInput.label) {
            await window.AudioEngine.requestAccess(nextInput.id);
          }
        }
        if (nextOutput) setOutputDeviceIdState(nextOutput.id);
      }
      return mapped;
    } catch (e) {
      setAudioDevices([]);
      setDeviceAccess("blocked");
      setDeviceNote("Audio access was blocked. Allow microphone access in the browser to list available devices.");
      return [];
    }
  }

  React.useEffect(() => {
    scanDevices(false);
    if (navigator.mediaDevices && navigator.mediaDevices.addEventListener) {
      const onChange = () => scanDevices(false);
      navigator.mediaDevices.addEventListener("devicechange", onChange);
      return () => navigator.mediaDevices.removeEventListener("devicechange", onChange);
    }
  }, []);

  const selectedInput = audioDevices.find(d => d.id === inputDeviceId && d.kind === "audioinput") || null;
  const selectedOutput = audioDevices.find(d => d.id === outputDeviceId && d.kind === "audiooutput") || null;
  const device = selectedInput ? selectedInput.label : "";
  const analysisInputLabel = selectedInput ? selectedInput.label : "";
  const playbackOutputLabel = selectedOutput ? selectedOutput.label : "";
  function setAnalysisInputId(id) {
    setInputDeviceIdState(id);
    const selected = audioDevices.find(d => d.id === id);
    if (selected && selected.kind === "audioinput" && deviceAccess === "granted") {
      window.AudioEngine.requestAccess(id).catch(() => {
        setDeviceAccess("blocked");
        setDeviceNote("Audio access was blocked. Allow microphone access in the browser to use this input.");
      });
    }
  }
  function setPlaybackOutputId(id) {
    setOutputDeviceIdState(id);
  }
  function setDeviceId(id) {
    const selected = audioDevices.find(d => d.id === id);
    if (selected && selected.kind === "audiooutput") setPlaybackOutputId(id);
    else setAnalysisInputId(id);
  }
  const isCurrent = viewDate === sessions[sessions.length - 1]?.date;
  const personKey = operator || "Unassigned";
  const results = ((resultsBySession[viewDate] || {})[personKey]) || {};

  const [runningKey, setRunningKey] = React.useState(null);
  const [runProgress, setRunProgress] = React.useState(0);
  const [runMode, setRunMode] = React.useState(null);
  const [runElapsed, setRunElapsed] = React.useState(0);
  const [queue, setQueue] = React.useState(null);
  const timers = React.useRef([]);
  const pending = React.useRef(null);

  function clearTimers() { timers.current.forEach(id => { clearTimeout(id); clearInterval(id); }); timers.current = []; }

  function saveResult(key, value) {
    setResultsBySession(prev => ({
      ...prev,
      [viewDate]: {
        ...(prev[viewDate] || {}),
        [personKey]: {
          ...(((prev[viewDate] || {})[personKey]) || {}),
          [key]: value,
        },
      },
    }));
  }

  function finalize(item, ctx) {
    const key = runKey(item, ctx);
    const graded = window.grading.gradeRun(item, ctx);
    const snap = window.AudioEngine.state;
    saveResult(key, {
      ...graded,
      ranAt: new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }),
      captured: { lufs: snap.lufs.toFixed(1), peak: snap.peak.toFixed(1), bass: snap.bass.toFixed(1), mid: snap.mid.toFixed(1), treble: snap.treble.toFixed(1) },
    });
  }

  function run(item, ctx, opts) {
    opts = opts || {};
    if (opts.fast) return runFast(item, ctx);
    if (pending.current) { finalize(pending.current.item, pending.current.ctx); }
    clearTimers();
    const key = runKey(item, ctx);
    pending.current = { item, ctx };
    setRunningKey(key);
    setRunMode("manual");
    setRunElapsed(0);
    const start = performance.now();
    const id = setInterval(() => setRunElapsed((performance.now() - start) / 1000), 100);
    timers.current.push(id);
  }

  function runFast(item, ctx) {
    const dur = 420;
    const key = runKey(item, ctx);
    setRunningKey(key); setRunMode("fast"); setRunProgress(0);
    const start = performance.now();
    const tickId = setInterval(() => {
      const p = Math.min(1, (performance.now() - start) / dur);
      setRunProgress(p); if (p >= 1) clearInterval(tickId);
    }, 50);
    timers.current.push(tickId);
    return new Promise(resolve => {
      const t = setTimeout(() => {
        clearInterval(tickId);
        finalize(item, ctx);
        setRunningKey(null); setRunMode(null); setRunProgress(0);
        resolve();
      }, dur);
      timers.current.push(t);
    });
  }

  function stop() {
    if (pending.current && runMode === "manual") finalize(pending.current.item, pending.current.ctx);
    pending.current = null;
    clearTimers();
    setRunningKey(null); setRunMode(null); setRunProgress(0); setRunElapsed(0); setQueue(null);
  }

  async function runAll(force) {
    const D = window.QA_DATA;
    const tasks = [];
    D.groups.forEach(g => {
      g.items.forEach(item => {
        if (g.kind === "matrix") item.songs.forEach(song => tasks.push([item, song]));
        else tasks.push([item, null]);
      });
    });
    const todo = force ? tasks : tasks.filter(([it, ctx]) => !results[runKey(it, ctx)]);
    setQueue({ total: todo.length, done: 0 });
    for (let i = 0; i < todo.length; i++) {
      await run(todo[i][0], todo[i][1], { fast: true });
      setQueue({ total: todo.length, done: i + 1 });
    }
    setQueue(null);
  }

  function reset() {
    clearTimers();
    setResultsBySession(prev => ({
      ...prev,
      [viewDate]: { ...(prev[viewDate] || {}), [personKey]: {} },
    }));
    setRunningKey(null); setQueue(null);
  }

  const stats = React.useMemo(() => {
    const D = window.QA_DATA;
    let done = 0, total = D.total, cells = 0, good = 0, low = 0, high = 0, warn = 0;
    const issues = [];
    D.groups.forEach(g => g.items.forEach(item => {
      const ctxs = g.kind === "matrix" ? item.songs : [null];
      ctxs.forEach(ctx => {
        const r = results[runKey(item, ctx)];
        if (r) {
          done++;
          Object.entries(r.cells).forEach(([crit, c]) => {
            cells++;
            if (c.grade === "good") good++;
            else if (c.grade === "low") low++;
            else if (c.grade === "high") high++;
            else if (c.grade === "warn") warn++;
            if (c.grade === "high" || c.grade === "warn") issues.push({ item: item.name + (ctx ? " · " + ctx : ""), crit, group: g.title, ...c });
          });
        }
      });
    }));
    const passRate = cells ? Math.round((good / cells) * 100) : 0;
    const health = cells ? Math.round(((good + low * 0.6) / cells) * 100) : 0;
    return { done, total, cells, good, low, high, warn, passRate, health, issues };
  }, [results]);

  const sessionHistory = React.useMemo(() => sessions.map(s => {
    const byPerson = resultsBySession[s.date] || {};
    const people = Object.keys(byPerson).filter(Boolean);
    let done = 0, cells = 0, good = 0, low = 0, high = 0, warn = 0;
    Object.values(byPerson).forEach(personResults => {
      Object.values(personResults || {}).forEach(r => {
        done++;
        Object.values(r.cells || {}).forEach(c => {
          cells++;
          if (c.grade === "good") good++;
          else if (c.grade === "low") low++;
          else if (c.grade === "high") high++;
          else if (c.grade === "warn") warn++;
        });
      });
    });
    return {
      ...s,
      operator: people.length ? people.join(", ") : (s.operator || "Unassigned"),
      health: cells ? Math.round(((good + low * 0.6) / cells) * 100) : 0,
      passRate: cells ? Math.round((good / cells) * 100) : 0,
      clips: high,
      issues: high + warn,
      current: s.date === sessions[sessions.length - 1]?.date,
    };
  }), [sessions, resultsBySession]);

  const val = {
    results, liveResults: results, resultsBySession, run, runAll, stop, reset,
    runningKey, runProgress, runMode, runElapsed, queue, stats, runKey,
    viewDate, setViewDate, isCurrent, sessions, currentSession, addSunday, sessionHistory,
    device, deviceId: inputDeviceId, setDeviceId, selectedDevice: selectedInput,
    inputDeviceId, outputDeviceId, setAnalysisInputId, setPlaybackOutputId,
    selectedInput, selectedOutput, analysisInputLabel, playbackOutputLabel,
    audioDevices, scanDevices, deviceAccess, deviceNote,
    operators, operator, setOperator, addOperator, personKey,
  };
  return <QACtx.Provider value={val}>{children}</QACtx.Provider>;
}

Object.assign(window, { QAProvider, useQA, runKey, nextSundayIso });
