// --- WALL-E Listening Skill: Recording Engine ---
// Core orchestrator for recording sessions. Manages lifecycle, coordinates
// storage (audio chunks on disk) and brain DB (metadata + transcript).

const fs = require('fs');
const path = require('path');
const { randomUUID: uuidv4 } = require('node:crypto');
const brain = require('../brain');
const storage = require('./storage');

// In-memory state: active recording ID -> { interimTexts: [], finalTexts: [] }
const _activeTranscripts = new Map();

// Validate recording ID is a proper UUID (prevents path traversal)
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function _validateId(id) {
  if (!id || !UUID_RE.test(id)) throw new Error('Invalid recording ID');
}

// ── Public API ────────────────────────────────────────────────────────

/**
 * Start a new recording session.
 * @param {string} mode - 'meeting' | 'dictation' | 'email'
 * @param {string} [title] - Human-readable title
 * @param {string} [calendarEventId] - Optional linked calendar event
 * @returns {{ id, mode, title, status, started_at }}
 */
function startRecording(mode, title, calendarEventId) {
  if (!mode) mode = 'meeting';
  const db = brain.getDb();

  // Only one recording can be active at a time — auto-stop stale ones
  const existing = db.prepare("SELECT id FROM recordings WHERE status = 'recording'").get();
  if (existing) {
    // Auto-stop stale recording (browser may have closed without stopping)
    db.prepare("UPDATE recordings SET status = 'stopped', stopped_at = ? WHERE id = ?")
      .run(new Date().toISOString(), existing.id);
    _activeTranscripts.delete(existing.id);
  }

  const id = uuidv4();
  const now = new Date().toISOString();
  const displayTitle = title || ('Recording ' + now.slice(0, 16).replace('T', ' '));

  db.prepare(`
    INSERT INTO recordings (id, title, mode, status, calendar_event_id, audio_format, started_at)
    VALUES (?, ?, ?, 'recording', ?, 'webm', ?)
  `).run(id, displayTitle, mode, calendarEventId || null, now);

  _activeTranscripts.set(id, { interimTexts: [], finalTexts: [], chunkIndex: 0 });

  return { id, mode, title: displayTitle, status: 'recording', started_at: now };
}

/**
 * Stop an active recording. Finalizes audio chunks and updates DB.
 */
function stopRecording(recordingId) {
  _validateId(recordingId);
  const db = brain.getDb();

  const rec = db.prepare('SELECT * FROM recordings WHERE id = ?').get(recordingId);
  if (!rec) throw new Error('Recording not found: ' + recordingId);
  if (rec.status !== 'recording') {
    throw new Error('Recording is not active (status: ' + rec.status + ')');
  }

  const now = new Date().toISOString();
  const startedAt = new Date(rec.started_at).getTime();
  const durationMs = Date.now() - startedAt;

  // Finalize audio file from chunks
  let audioPath = null;
  let fileSize = 0;
  try {
    const result = storage.finalizeRecording(recordingId);
    audioPath = result.path;
    fileSize = result.size;
  } catch (err) {
    // No chunks uploaded — ok for very short recordings
    if (err.message !== 'Recording directory not found' && err.message !== 'No chunks found') {
      throw err;
    }
  }

  // Flush accumulated interim transcript text
  const transcriptBuf = _activeTranscripts.get(recordingId);
  const interimTranscript = transcriptBuf
    ? transcriptBuf.finalTexts.join(' ').trim() || null
    : null;
  _activeTranscripts.delete(recordingId);

  db.prepare(`
    UPDATE recordings SET status = 'stopped', stopped_at = ?, duration_ms = ?,
    audio_path = ?, file_size_bytes = ? WHERE id = ?
  `).run(now, durationMs, audioPath, fileSize, recordingId);

  // Store interim transcript if we have one
  if (interimTranscript) {
    const transcriptId = uuidv4();
    db.prepare(`
      INSERT INTO transcripts (id, recording_id, provider, full_text, word_count, status)
      VALUES (?, ?, 'web-speech', ?, ?, 'done')
    `).run(transcriptId, recordingId, interimTranscript, interimTranscript.split(/\s+/).length);
  }

  // Auto-trigger cloud transcription if API key is available and audio exists
  let transcribing = false;
  if (audioPath) {
    try {
      const stt = require('./stt');
      if (stt.hasApiKey()) {
        transcribing = true;
        // Fire-and-forget — runs in background, progress tracked via SSE
        stt.transcribeRecording(recordingId).catch(err => {
          console.error('[listening] Auto-transcription failed:', err.message);
        });
      }
    } catch {}
  }

  return {
    id: recordingId,
    status: transcribing ? 'transcribing' : 'stopped',
    duration_ms: durationMs,
    file_size_bytes: fileSize,
    transcript: interimTranscript
  };
}

/**
 * Upload an audio chunk for an active recording.
 */
function uploadChunk(recordingId, buffer) {
  _validateId(recordingId);
  const db = brain.getDb();

  const rec = db.prepare('SELECT status FROM recordings WHERE id = ?').get(recordingId);
  if (!rec) throw new Error('Recording not found: ' + recordingId);
  if (rec.status !== 'recording') {
    throw new Error('Cannot upload chunk: recording is not active');
  }

  // Get and increment chunk index (initialize buffer if server restarted mid-recording)
  let buf = _activeTranscripts.get(recordingId);
  if (!buf) {
    buf = { interimTexts: [], finalTexts: [], chunkIndex: 0 };
    _activeTranscripts.set(recordingId, buf);
  }
  const index = buf.chunkIndex++;

  const result = storage.saveChunk(recordingId, buffer, index);

  // Update file size
  const dir = storage.getRecordingDir(recordingId);
  let totalSize = 0;
  try {
    for (const f of fs.readdirSync(dir)) {
      if (f.startsWith('chunk-')) totalSize += fs.statSync(path.join(dir, f)).size;
    }
  } catch {}

  return { index, size: result.size, totalSize };
}

/**
 * Process interim or final speech-to-text results from Web Speech API.
 */
function processInterimText(recordingId, text, isFinal) {
  _validateId(recordingId);
  if (!text || !text.trim()) return;

  let buf = _activeTranscripts.get(recordingId);
  if (!buf) {
    // Only track if this recording is genuinely active in the DB (prevents orphan entries)
    const rec = brain.getDb().prepare("SELECT id FROM recordings WHERE id = ? AND status = 'recording'").get(recordingId);
    if (!rec) return;
    buf = { interimTexts: [], finalTexts: [], chunkIndex: 0 };
    _activeTranscripts.set(recordingId, buf);
  }

  if (isFinal) {
    buf.finalTexts.push(text.trim());
    buf.interimTexts = [];
  } else {
    buf.interimTexts = [text.trim()];
  }
}

/**
 * Get the currently active recording, if any.
 */
function getActiveRecording() {
  const db = brain.getDb();
  const rec = db.prepare("SELECT * FROM recordings WHERE status = 'recording' LIMIT 1").get();
  if (!rec) return null;

  const buf = _activeTranscripts.get(rec.id);
  if (buf) {
    const finals = buf.finalTexts.join(' ');
    const interim = buf.interimTexts.join(' ');
    rec.liveTranscript = (finals + (interim ? ' ' + interim : '')).trim() || null;
  }
  return rec;
}

/**
 * Get a recording by ID.
 */
function getRecording(id) {
  _validateId(id);
  return brain.getDb().prepare('SELECT * FROM recordings WHERE id = ?').get(id) || null;
}

/**
 * Get recording with transcript and segments (for detail view).
 */
function getRecordingDetail(id) {
  _validateId(id);
  const db = brain.getDb();
  const recording = db.prepare('SELECT * FROM recordings WHERE id = ?').get(id);
  if (!recording) return null;

  const transcript = db.prepare(
    'SELECT * FROM transcripts WHERE recording_id = ? ORDER BY created_at DESC LIMIT 1'
  ).get(id) || null;

  let segments = [];
  if (transcript) {
    segments = db.prepare(
      'SELECT * FROM transcript_segments WHERE transcript_id = ? ORDER BY seq_order'
    ).all(transcript.id);
  }

  return { recording, transcript, segments };
}

/**
 * List recordings with optional filters.
 */
function listRecordings(opts) {
  if (!opts) opts = {};
  const db = brain.getDb();

  const conditions = [];
  const params = [];

  if (opts.status) { conditions.push('status = ?'); params.push(opts.status); }
  if (opts.mode) { conditions.push('mode = ?'); params.push(opts.mode); }

  const where = conditions.length > 0 ? 'WHERE ' + conditions.join(' AND ') : '';
  const limit = opts.limit || 50;
  const offset = opts.offset || 0;

  return db.prepare(
    'SELECT * FROM recordings ' + where + ' ORDER BY started_at DESC LIMIT ? OFFSET ?'
  ).all(...params, limit, offset);
}

/**
 * Delete a recording — removes DB rows and all files on disk.
 */
function deleteRecording(id) {
  _validateId(id);
  const db = brain.getDb();

  const rec = db.prepare('SELECT id, status FROM recordings WHERE id = ?').get(id);
  if (!rec) return { deleted: false };

  if (rec.status === 'recording') _activeTranscripts.delete(id);

  // Remove DB rows in a transaction for consistency
  db.transaction(() => {
    const transcripts = db.prepare('SELECT id FROM transcripts WHERE recording_id = ?').all(id);
    for (const t of transcripts) {
      db.prepare('DELETE FROM transcript_segments WHERE transcript_id = ?').run(t.id);
    }
    db.prepare('DELETE FROM transcripts WHERE recording_id = ?').run(id);
    db.prepare('DELETE FROM recordings WHERE id = ?').run(id);
  })();

  // Remove files from disk (outside transaction — not rollback-able)
  storage.deleteRecording(id);

  return { deleted: true };
}

module.exports = {
  startRecording,
  stopRecording,
  uploadChunk,
  processInterimText,
  getActiveRecording,
  getRecording,
  getRecordingDetail,
  listRecordings,
  deleteRecording
};
