// Brook Consultancy — Native recording (v2 pass 4)
//
// One-button mic capture on the consultant's tablet. On Stop, the audio blob
// uploads to the same Shared Drive as the consultation JSON, gets linked to
// the consultation record by `record.audio.fileId`, and surfaces to the
// operator inside AI Assist with playback + a copy-Drive-link button.
//
// Architecture choices (locked in by the user):
//   - Microphone only (no fancy stereo / lavalier capture)
//   - Browser default container (WebM/Opus on Chromium/FF, MP4/AAC on Safari)
//   - Upload kicks off on Stop, in the background, single resumable upload
//   - Audio file is SEPARATE from the JSON record on Drive
//   - Soft recording indicator (no consent modal)
//   - Separate red "● Recording" pill next to the timer
//   - Timer pause/resume pauses/resumes the recording
//
// ---------------------------------------------------------------
// Constraints worth remembering
// ---------------------------------------------------------------
//   - The MediaRecorder instance cannot survive a page reload. If the page is
//     refreshed mid-recording, the audio captured before reload is lost. We
//     persist a marker so the UI can show "Recording was interrupted" on the
//     restored record.
//   - The MediaStream must be stopped (tracks released) on stop AND on unmount,
//     otherwise the browser keeps the mic indicator on indefinitely.
//   - iPad Safari only supports MP4/AAC — we negotiate mimeType to whatever
//     the runtime offers, falling back to the browser default.
//   - Drive resumable uploads need an OAuth token. If the operator hasn't
//     connected this device, we hold the blob in memory and surface a clear
//     error rather than dropping the recording.

// ---------------------------------------------------------------
// Persistence — recording state survives navigation but NOT reload
// (MediaRecorder can't be serialised). We do persist:
//   - presence of an in-flight recording (so we can warn on reload)
//   - completed-but-not-yet-uploaded blobs keyed by recordId (in IndexedDB)
//   - upload progress (per-record, in localStorage)
// ---------------------------------------------------------------

const RECORDER_STATE_KEY = 'brook_recorder_state_v1';   // localStorage: {[recordId]: 'recording'|'paused'|'stopped'|'uploading'|'failed'|'complete'}
const RECORDER_UPLOAD_KEY = 'brook_recorder_upload_v1'; // localStorage: {[recordId]: {progress, fileId, error, size, mimeType}}
const RECORDER_BLOB_DB = 'brook_recorder_blobs_v1';     // IndexedDB DB name
const RECORDER_BLOB_STORE = 'blobs';

function _readRecState() { try { return JSON.parse(localStorage.getItem(RECORDER_STATE_KEY) || '{}'); } catch { return {}; } }
function _writeRecState(s) { try { localStorage.setItem(RECORDER_STATE_KEY, JSON.stringify(s)); } catch {} }
function _readUploadState() { try { return JSON.parse(localStorage.getItem(RECORDER_UPLOAD_KEY) || '{}'); } catch { return {}; } }
function _writeUploadState(s) { try { localStorage.setItem(RECORDER_UPLOAD_KEY, JSON.stringify(s)); } catch {} }

// ---------------------------------------------------------------
// IndexedDB blob store — small wrapper so blobs survive navigation
// inside the SPA but not full reloads (which is fine, we re-upload from
// memory; if reload happens we surface a clear "recording lost" notice).
// ---------------------------------------------------------------
function _openDb() {
  return new Promise((resolve, reject) => {
    const req = indexedDB.open(RECORDER_BLOB_DB, 1);
    req.onupgradeneeded = () => {
      const db = req.result;
      if (!db.objectStoreNames.contains(RECORDER_BLOB_STORE)) {
        db.createObjectStore(RECORDER_BLOB_STORE);
      }
    };
    req.onsuccess = () => resolve(req.result);
    req.onerror = () => reject(req.error);
  });
}
async function _putBlob(recordId, blob) {
  const db = await _openDb();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(RECORDER_BLOB_STORE, 'readwrite');
    tx.objectStore(RECORDER_BLOB_STORE).put(blob, recordId);
    tx.oncomplete = () => { db.close(); resolve(); };
    tx.onerror = () => { db.close(); reject(tx.error); };
  });
}
async function _getBlob(recordId) {
  const db = await _openDb();
  return new Promise((resolve, reject) => {
    const tx = db.transaction(RECORDER_BLOB_STORE, 'readonly');
    const req = tx.objectStore(RECORDER_BLOB_STORE).get(recordId);
    req.onsuccess = () => { db.close(); resolve(req.result || null); };
    req.onerror = () => { db.close(); reject(req.error); };
  });
}
async function _deleteBlob(recordId) {
  try {
    const db = await _openDb();
    return new Promise((resolve) => {
      const tx = db.transaction(RECORDER_BLOB_STORE, 'readwrite');
      tx.objectStore(RECORDER_BLOB_STORE).delete(recordId);
      tx.oncomplete = () => { db.close(); resolve(); };
      tx.onerror = () => { db.close(); resolve(); };
    });
  } catch {}
}

// ---------------------------------------------------------------
// mimeType negotiation — return whatever the runtime supports
// ---------------------------------------------------------------
function _pickMimeType() {
  if (typeof MediaRecorder === 'undefined') return null;
  const candidates = [
    'audio/webm;codecs=opus',
    'audio/webm',
    'audio/ogg;codecs=opus',
    'audio/mp4;codecs=mp4a.40.2',
    'audio/mp4',
    'audio/aac'
  ];
  for (const c of candidates) {
    try { if (MediaRecorder.isTypeSupported(c)) return c; } catch {}
  }
  return ''; // fallback: let the browser pick
}

function _extForMime(mime) {
  if (!mime) return 'bin';
  if (mime.includes('webm')) return 'webm';
  if (mime.includes('ogg')) return 'ogg';
  if (mime.includes('mp4')) return 'm4a';
  if (mime.includes('aac')) return 'aac';
  return 'audio';
}

// ---------------------------------------------------------------
// Drive resumable upload (audio blobs can be 5-15 MB; resumable is the
// safe path even if we only do it in one POST today).
// ---------------------------------------------------------------
async function uploadAudioToDrive({ folderId, driveId, recordId, blob, mimeType, onProgress }) {
  if (!window.hasValidToken || !window.hasValidToken()) {
    throw new Error('Drive sign-in required to upload audio.');
  }
  const token = window.getAccessToken();

  const metadata = {
    name: `audio-${recordId}.${_extForMime(mimeType)}`,
    mimeType: mimeType || 'application/octet-stream',
    parents: [folderId],
    driveId,
    appProperties: {
      recordId,
      kind: 'audio',
      capturedAt: new Date().toISOString(),
      sizeBytes: String(blob.size)
    }
  };

  const initParams = new URLSearchParams({
    uploadType: 'resumable',
    supportsAllDrives: 'true',
    fields: 'id,name,size,mimeType,webContentLink,webViewLink'
  });

  // Step 1: initiate the resumable session
  const initRes = await fetch(`https://www.googleapis.com/upload/drive/v3/files?${initParams}`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json; charset=UTF-8',
      'X-Upload-Content-Type': metadata.mimeType,
      'X-Upload-Content-Length': String(blob.size)
    },
    body: JSON.stringify(metadata)
  });
  if (!initRes.ok) {
    const body = await initRes.text().catch(() => '');
    throw new Error(`Resumable init failed (${initRes.status}): ${body || initRes.statusText}`);
  }
  const sessionUri = initRes.headers.get('Location') || initRes.headers.get('location');
  if (!sessionUri) throw new Error('Drive did not return a resumable upload URL.');

  // Step 2: upload the body with progress via XHR (fetch doesn't expose progress)
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open('PUT', sessionUri, true);
    xhr.setRequestHeader('Content-Type', metadata.mimeType);
    xhr.upload.onprogress = (e) => {
      if (e.lengthComputable && onProgress) {
        onProgress(Math.round((e.loaded / e.total) * 100));
      }
    };
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        try { resolve(JSON.parse(xhr.responseText)); }
        catch { resolve({ id: null, raw: xhr.responseText }); }
      } else {
        reject(new Error(`Audio upload failed (${xhr.status}): ${xhr.responseText || xhr.statusText}`));
      }
    };
    xhr.onerror = () => reject(new Error('Network error during audio upload.'));
    xhr.send(blob);
  });
}

// ---------------------------------------------------------------
// useRecorder — the hook that powers the Record / Stop / Pause / Resume
// buttons in the header.
//
// Returns:
//   { state, durationSec, level, mimeType, error, sizeBytes,
//     start, stop, pauseRec, resumeRec,
//     uploadProgress, uploadFileId, retryUpload }
//
//   state ∈ 'idle' | 'requesting' | 'recording' | 'paused' | 'stopping' |
//            'uploading' | 'complete' | 'failed' | 'interrupted'
//
// CRITICAL: every setState call is functional. The MediaStream lives in a
// ref so React doesn't try to re-render around it.
// ---------------------------------------------------------------
function useRecorder({ record, store }) {
  const recordId = record?._id || null;
  const audioMeta = record?.audio || null;
  const initialState = audioMeta?.fileId ? 'complete' : 'idle';

  const [state, setState] = React.useState(initialState);
  const [durationSec, setDurationSec] = React.useState(audioMeta?.durationSec || 0);
  const [level, setLevel] = React.useState(0); // 0..1 audio level meter
  const [error, setError] = React.useState(null);
  const [sizeBytes, setSizeBytes] = React.useState(audioMeta?.sizeBytes || 0);
  const [uploadProgress, setUploadProgress] = React.useState(0);

  // Refs that must NOT trigger re-renders
  const recRef = React.useRef(null);          // MediaRecorder
  const streamRef = React.useRef(null);       // MediaStream
  const chunksRef = React.useRef([]);         // collected Blob parts
  const startedAtRef = React.useRef(null);    // ms timestamp of recording start
  const accumulatedRef = React.useRef(0);     // ms of recording elapsed before current run
  const tickRef = React.useRef(null);         // setInterval id
  const meterRafRef = React.useRef(null);     // requestAnimationFrame for level meter
  const audioCtxRef = React.useRef(null);
  const analyserRef = React.useRef(null);
  const mimeTypeRef = React.useRef(null);

  // When the active record changes, reset hook state to that record's audio status
  React.useEffect(() => {
    if (!recordId) { setState('idle'); setDurationSec(0); setSizeBytes(0); setUploadProgress(0); setError(null); return; }
    if (audioMeta?.fileId) { setState('complete'); setDurationSec(audioMeta.durationSec || 0); setSizeBytes(audioMeta.sizeBytes || 0); return; }
    // Check persisted state from a possible page reload
    const persisted = _readRecState()[recordId];
    if (persisted === 'recording' || persisted === 'paused') {
      // MediaRecorder didn't survive the reload — surface that.
      setState('interrupted');
      setError('Recording was interrupted by a page reload. Any audio captured before the reload was lost.');
      const next = _readRecState(); delete next[recordId]; _writeRecState(next);
    } else if (persisted === 'uploading' || persisted === 'failed') {
      // Try to recover a not-yet-uploaded blob
      setState(persisted);
      setUploadProgress(_readUploadState()[recordId]?.progress || 0);
    } else {
      setState('idle');
      setDurationSec(0);
      setSizeBytes(0);
      setUploadProgress(0);
      setError(null);
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [recordId]);

  // Cleanup on unmount: release the mic, kill timers
  React.useEffect(() => () => _teardown(), []);

  function _teardown() {
    if (tickRef.current) { clearInterval(tickRef.current); tickRef.current = null; }
    if (meterRafRef.current) { cancelAnimationFrame(meterRafRef.current); meterRafRef.current = null; }
    try { if (audioCtxRef.current) audioCtxRef.current.close(); } catch {}
    audioCtxRef.current = null;
    analyserRef.current = null;
    try {
      if (recRef.current && recRef.current.state !== 'inactive') recRef.current.stop();
    } catch {}
    recRef.current = null;
    if (streamRef.current) {
      streamRef.current.getTracks().forEach(t => { try { t.stop(); } catch {} });
      streamRef.current = null;
    }
  }

  function _startMeter(stream) {
    try {
      const AC = window.AudioContext || window.webkitAudioContext;
      if (!AC) return;
      const ctx = new AC();
      const src = ctx.createMediaStreamSource(stream);
      const analyser = ctx.createAnalyser();
      analyser.fftSize = 256;
      src.connect(analyser);
      audioCtxRef.current = ctx;
      analyserRef.current = analyser;
      const buf = new Uint8Array(analyser.frequencyBinCount);
      const loop = () => {
        if (!analyserRef.current) return;
        analyserRef.current.getByteTimeDomainData(buf);
        // Compute RMS
        let sum = 0;
        for (let i = 0; i < buf.length; i++) {
          const v = (buf[i] - 128) / 128;
          sum += v * v;
        }
        const rms = Math.sqrt(sum / buf.length);
        setLevel(Math.min(1, rms * 2.5));
        meterRafRef.current = requestAnimationFrame(loop);
      };
      loop();
    } catch (e) {
      console.warn('Audio meter unavailable:', e);
    }
  }

  function _startTicker() {
    if (tickRef.current) clearInterval(tickRef.current);
    tickRef.current = setInterval(() => {
      if (!startedAtRef.current) return;
      const elapsed = accumulatedRef.current + (Date.now() - startedAtRef.current);
      setDurationSec(Math.floor(elapsed / 1000));
    }, 500);
  }

  // ----- start -----
  const start = React.useCallback(async () => {
    if (!recordId) { setError('Open a consultation first.'); return; }
    if (state === 'recording' || state === 'paused') return;
    setError(null);
    setState('requesting');
    try {
      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
        throw new Error('This browser does not support microphone capture.');
      }
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: {
          echoCancellation: true,
          noiseSuppression: true,
          autoGainControl: true
        }
      });
      streamRef.current = stream;

      const mime = _pickMimeType();
      mimeTypeRef.current = mime;
      const rec = mime
        ? new MediaRecorder(stream, { mimeType: mime })
        : new MediaRecorder(stream);
      recRef.current = rec;
      chunksRef.current = [];

      rec.ondataavailable = (e) => { if (e.data && e.data.size > 0) chunksRef.current.push(e.data); };
      rec.onerror = (e) => {
        console.error('MediaRecorder error', e);
        setError('Recording error: ' + (e.error?.message || 'unknown'));
        setState('failed');
        _teardown();
      };

      rec.start(1000); // 1s timeslice gives us frequent data callbacks
      startedAtRef.current = Date.now();
      accumulatedRef.current = 0;
      setDurationSec(0);
      setSizeBytes(0);
      _startTicker();
      _startMeter(stream);

      const next = _readRecState(); next[recordId] = 'recording'; _writeRecState(next);
      setState('recording');
    } catch (e) {
      console.warn('Recorder start failed', e);
      const msg = e.name === 'NotAllowedError'
        ? 'Microphone access was denied. Allow microphone permission for this site and try again.'
        : e.name === 'NotFoundError'
        ? 'No microphone found on this device.'
        : e.message || String(e);
      setError(msg);
      setState('failed');
      if (streamRef.current) { streamRef.current.getTracks().forEach(t => t.stop()); streamRef.current = null; }
    }
  }, [recordId, state]);

  // ----- pause / resume -----
  const pauseRec = React.useCallback(() => {
    const rec = recRef.current;
    if (!rec || rec.state !== 'recording') return;
    try { rec.pause(); } catch {}
    if (startedAtRef.current) {
      accumulatedRef.current += (Date.now() - startedAtRef.current);
      startedAtRef.current = null;
    }
    if (tickRef.current) { clearInterval(tickRef.current); tickRef.current = null; }
    if (meterRafRef.current) { cancelAnimationFrame(meterRafRef.current); meterRafRef.current = null; }
    setLevel(0);
    const next = _readRecState(); if (recordId) next[recordId] = 'paused'; _writeRecState(next);
    setState('paused');
  }, [recordId]);

  const resumeRec = React.useCallback(() => {
    const rec = recRef.current;
    if (!rec || rec.state !== 'paused') return;
    try { rec.resume(); } catch {}
    startedAtRef.current = Date.now();
    _startTicker();
    if (streamRef.current) _startMeter(streamRef.current);
    const next = _readRecState(); if (recordId) next[recordId] = 'recording'; _writeRecState(next);
    setState('recording');
  }, [recordId]);

  // ----- stop + upload -----
  const stop = React.useCallback(async () => {
    const rec = recRef.current;
    if (!rec || rec.state === 'inactive') return;
    setState('stopping');
    if (tickRef.current) { clearInterval(tickRef.current); tickRef.current = null; }
    if (meterRafRef.current) { cancelAnimationFrame(meterRafRef.current); meterRafRef.current = null; }

    // Wait for the final dataavailable
    const stopped = new Promise(resolve => { rec.onstop = () => resolve(); });
    try { rec.stop(); } catch {}
    await stopped;

    // Capture final duration
    if (startedAtRef.current) {
      accumulatedRef.current += (Date.now() - startedAtRef.current);
      startedAtRef.current = null;
    }
    const finalDuration = Math.floor(accumulatedRef.current / 1000);
    setDurationSec(finalDuration);

    // Release mic
    if (streamRef.current) {
      streamRef.current.getTracks().forEach(t => { try { t.stop(); } catch {} });
      streamRef.current = null;
    }

    const mime = mimeTypeRef.current || rec.mimeType || 'audio/webm';
    const blob = new Blob(chunksRef.current, { type: mime });
    chunksRef.current = [];
    setSizeBytes(blob.size);

    // Persist the blob so a navigation away+back doesn't lose it
    if (recordId) {
      try { await _putBlob(recordId, blob); } catch (e) { console.warn('Blob persist failed', e); }
    }

    // Mark uploading
    const ups = _readUploadState(); if (recordId) ups[recordId] = { progress: 0, mimeType: mime, size: blob.size }; _writeUploadState(ups);
    const recState = _readRecState(); if (recordId) recState[recordId] = 'uploading'; _writeRecState(recState);
    setState('uploading');

    // Stamp the record with audio metadata (no fileId yet)
    store.updateActive(d => ({
      ...d,
      audio: {
        ...(d.audio || {}),
        durationSec: finalDuration,
        sizeBytes: blob.size,
        mimeType: mime,
        capturedAt: new Date().toISOString(),
        uploadStatus: 'uploading',
        uploadProgress: 0,
        fileId: null,
        webViewLink: null
      }
    }));

    await _doUpload(blob, mime, finalDuration);
  }, [recordId, store]);

  // ----- upload implementation (also used by retryUpload) -----
  async function _doUpload(blob, mime, finalDuration) {
    setUploadProgress(0);
    setError(null);
    try {
      const settings = window.loadDriveSettings();
      if (!settings.clientId || !settings.sharedDriveId) {
        throw new Error('Drive sync is not configured. Open Sync Settings to set it up before uploading recordings.');
      }
      if (!window.hasValidToken()) {
        await window.ensureToken(settings.clientId);
      }
      const main = await window.findOrCreateFolder(window.BROOK_MAIN_FOLDER, settings.sharedDriveId, settings.sharedDriveId);
      const audioFolder = await window.findOrCreateFolder('audio', main, settings.sharedDriveId);
      const result = await uploadAudioToDrive({
        folderId: audioFolder,
        driveId: settings.sharedDriveId,
        recordId,
        blob,
        mimeType: mime,
        onProgress: (p) => {
          setUploadProgress(p);
          const ups = _readUploadState();
          if (recordId && ups[recordId]) { ups[recordId].progress = p; _writeUploadState(ups); }
          // Patch the in-flight record so the consultant view reflects progress
          store.updateActive(d => d._id === recordId ? ({
            ...d,
            audio: { ...(d.audio || {}), uploadProgress: p, uploadStatus: 'uploading' }
          }) : d);
        }
      });
      // Persist final upload result on the record
      store.updateActive(d => d._id === recordId ? ({
        ...d,
        audio: {
          ...(d.audio || {}),
          durationSec: finalDuration,
          sizeBytes: blob.size,
          mimeType: mime,
          uploadStatus: 'complete',
          uploadProgress: 100,
          fileId: result.id,
          webViewLink: result.webViewLink || null,
          uploadedAt: new Date().toISOString()
        },
        updatedAt: new Date().toISOString()
      }) : d);

      // Cleanup persisted blob + state
      if (recordId) {
        await _deleteBlob(recordId);
        const r = _readRecState(); delete r[recordId]; _writeRecState(r);
        const u = _readUploadState(); delete u[recordId]; _writeUploadState(u);
      }
      setUploadProgress(100);
      setState('complete');
    } catch (e) {
      console.warn('Audio upload failed', e);
      setError(e.message || String(e));
      const r = _readRecState(); if (recordId) r[recordId] = 'failed'; _writeRecState(r);
      const u = _readUploadState();
      if (recordId) { u[recordId] = { ...(u[recordId] || {}), error: e.message || String(e) }; _writeUploadState(u); }
      store.updateActive(d => d._id === recordId ? ({
        ...d,
        audio: { ...(d.audio || {}), uploadStatus: 'failed', uploadError: e.message || String(e) }
      }) : d);
      setState('failed');
    }
  }

  // ----- retry upload after a failure -----
  const retryUpload = React.useCallback(async () => {
    if (!recordId) return;
    const blob = await _getBlob(recordId);
    if (!blob) {
      setError('Cannot retry — the local audio blob is no longer available on this device.');
      return;
    }
    setState('uploading');
    const finalDuration = durationSec;
    await _doUpload(blob, blob.type, finalDuration);
  }, [recordId, durationSec]);

  return {
    state, durationSec, level, error, sizeBytes,
    mimeType: mimeTypeRef.current,
    start, stop, pauseRec, resumeRec, retryUpload,
    uploadProgress
  };
}

// Format helpers
function fmtDuration(s) {
  s = Math.max(0, Math.floor(s || 0));
  const m = Math.floor(s / 60);
  const sec = s % 60;
  return `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
}
function fmtBytes(b) {
  if (!b) return '0 KB';
  if (b < 1024) return b + ' B';
  if (b < 1024 * 1024) return Math.round(b / 1024) + ' KB';
  return (b / (1024 * 1024)).toFixed(1) + ' MB';
}

Object.assign(window, { useRecorder, fmtDuration, fmtBytes });
