// --- WALL-E Listening Skill: Speech-to-Text (AssemblyAI) ---
// Handles audio upload, transcription submission, polling, and result storage.
// Supports speaker diarization via speaker_labels.

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

const ASSEMBLYAI_BASE = 'https://api.assemblyai.com';
const POLL_INTERVAL_MS = 3000;
const MAX_POLL_ATTEMPTS = 200; // ~10 minutes max

// In-memory progress tracking for SSE subscribers
const _progress = new Map(); // recordingId -> { status, percent, message, updatedAt }

// ── API Key ──────────────────────────────────────────────────────────

function _getApiKey() {
  if (process.env.ASSEMBLYAI_API_KEY) return process.env.ASSEMBLYAI_API_KEY;
  try {
    const kv = brain.getKv('assemblyai_api_key');
    if (kv) return kv;
  } catch {}
  return null;
}

function hasApiKey() {
  return !!_getApiKey();
}

function setApiKey(key) {
  brain.setKv('assemblyai_api_key', key);
}

// ── Progress Tracking ────────────────────────────────────────────────

function _setProgress(recordingId, status, percent, message) {
  _progress.set(recordingId, { status, percent, message, updatedAt: Date.now() });
}

function getProgress(recordingId) {
  return _progress.get(recordingId) || null;
}

// ── AssemblyAI API Calls ─────────────────────────────────────────────

async function _uploadAudio(audioPath) {
  const apiKey = _getApiKey();
  if (!apiKey) throw new Error('AssemblyAI API key not configured');

  const fileBuffer = fs.readFileSync(audioPath);
  const resp = await fetch(ASSEMBLYAI_BASE + '/v2/upload', {
    method: 'POST',
    headers: {
      'Authorization': apiKey,
      'Content-Type': 'application/octet-stream'
    },
    body: fileBuffer
  });

  if (!resp.ok) {
    const text = await resp.text();
    throw new Error('AssemblyAI upload failed (' + resp.status + '): ' + text);
  }

  const data = await resp.json();
  return data.upload_url;
}

async function _submitTranscription(uploadUrl, opts) {
  const apiKey = _getApiKey();
  const body = {
    audio_url: uploadUrl,
    speaker_labels: opts.speakerLabels !== false,
    language_detection: true,
    punctuate: true,
    format_text: true
  };
  if (opts.speakersExpected) body.speakers_expected = opts.speakersExpected;

  const resp = await fetch(ASSEMBLYAI_BASE + '/v2/transcript', {
    method: 'POST',
    headers: {
      'Authorization': apiKey,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  });

  if (!resp.ok) {
    const text = await resp.text();
    throw new Error('AssemblyAI submit failed (' + resp.status + '): ' + text);
  }

  return resp.json();
}

async function _pollTranscript(transcriptId) {
  const apiKey = _getApiKey();
  const resp = await fetch(ASSEMBLYAI_BASE + '/v2/transcript/' + transcriptId, {
    headers: { 'Authorization': apiKey }
  });
  if (!resp.ok) {
    const text = await resp.text();
    throw new Error('AssemblyAI poll failed (' + resp.status + '): ' + text);
  }
  return resp.json();
}

// ── Main Transcription Pipeline ──────────────────────────────────────

/**
 * Transcribe a recording's audio file using AssemblyAI.
 * Stores results in transcripts + transcript_segments tables.
 *
 * @param {string} recordingId - UUID of the recording
 * @param {object} [opts] - Options
 * @param {boolean} [opts.speakerLabels=true] - Enable speaker diarization
 * @param {number} [opts.speakersExpected] - Expected number of speakers
 * @returns {Promise<{transcriptId, text, segments, wordCount, confidence, processingMs}>}
 */
async function transcribeRecording(recordingId, opts) {
  opts = opts || {};
  const db = brain.getDb();
  const startTime = Date.now();

  // Get recording info
  const rec = db.prepare('SELECT * FROM recordings WHERE id = ?').get(recordingId);
  if (!rec) throw new Error('Recording not found: ' + recordingId);
  if (!rec.audio_path || !fs.existsSync(rec.audio_path)) {
    throw new Error('No audio file found for recording: ' + recordingId);
  }

  // Update recording status
  db.prepare("UPDATE recordings SET status = 'transcribing' WHERE id = ?").run(recordingId);
  _setProgress(recordingId, 'uploading', 10, 'Uploading audio to AssemblyAI...');

  try {
    // Step 1: Upload audio
    const uploadUrl = await _uploadAudio(rec.audio_path);
    _setProgress(recordingId, 'submitted', 25, 'Audio uploaded. Submitting for transcription...');

    // Step 2: Submit transcription
    const submission = await _submitTranscription(uploadUrl, {
      speakerLabels: opts.speakerLabels !== false,
      speakersExpected: opts.speakersExpected
    });
    const aaiTranscriptId = submission.id;
    _setProgress(recordingId, 'processing', 30, 'Transcription in progress...');

    // Step 3: Poll for completion
    let result;
    for (let attempt = 0; attempt < MAX_POLL_ATTEMPTS; attempt++) {
      await _sleep(POLL_INTERVAL_MS);
      result = await _pollTranscript(aaiTranscriptId);

      if (result.status === 'completed') {
        _setProgress(recordingId, 'saving', 90, 'Transcription complete. Saving results...');
        break;
      }
      if (result.status === 'error') {
        throw new Error('AssemblyAI transcription error: ' + (result.error || 'unknown'));
      }

      // Update progress based on polling attempts (30-85% range)
      const pct = Math.min(85, 30 + Math.floor((attempt / MAX_POLL_ATTEMPTS) * 55));
      _setProgress(recordingId, 'processing', pct, 'Transcription in progress...');
    }

    if (!result || result.status !== 'completed') {
      throw new Error('Transcription timed out after polling');
    }

    // Step 4: Store results
    const processingMs = Date.now() - startTime;
    const transcriptId = uuidv4();
    const fullText = result.text || '';
    const wordCount = fullText.split(/\s+/).filter(Boolean).length;
    const confidence = result.confidence || null;
    const language = result.language_code || 'en';

    db.prepare(`
      INSERT INTO transcripts (id, recording_id, provider, full_text, confidence, language, word_count, processing_ms, status)
      VALUES (?, ?, 'assemblyai', ?, ?, ?, ?, ?, 'done')
    `).run(transcriptId, recordingId, fullText, confidence, language, wordCount, processingMs);

    // Step 5: Store speaker-labeled segments (utterances)
    let segments = [];
    if (result.utterances && result.utterances.length > 0) {
      const insert = db.prepare(`
        INSERT INTO transcript_segments (id, transcript_id, speaker_id, speaker_label, text, start_ms, end_ms, confidence, seq_order)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
      `);

      const insertMany = db.transaction((utterances) => {
        utterances.forEach((utt, i) => {
          const segId = uuidv4();
          const label = 'Speaker ' + (utt.speaker || String.fromCharCode(65 + i));
          insert.run(segId, transcriptId, null, label, utt.text, utt.start, utt.end, utt.confidence || null, i);
          segments.push({ id: segId, speaker_label: label, text: utt.text, start_ms: utt.start, end_ms: utt.end });
        });
      });
      insertMany(result.utterances);
    }

    // Update recording status to done
    db.prepare("UPDATE recordings SET status = 'done' WHERE id = ?").run(recordingId);
    _setProgress(recordingId, 'done', 100, 'Transcription complete.');

    return { transcriptId, text: fullText, segments, wordCount, confidence, processingMs };

  } catch (err) {
    // Store error state
    db.prepare("UPDATE recordings SET status = 'error' WHERE id = ?").run(recordingId);
    _setProgress(recordingId, 'error', 0, err.message);

    // Store error transcript record
    const errorTranscriptId = uuidv4();
    db.prepare(`
      INSERT INTO transcripts (id, recording_id, provider, full_text, status, error_message)
      VALUES (?, ?, 'assemblyai', '', 'error', ?)
    `).run(errorTranscriptId, recordingId, err.message);

    throw err;
  }
}

/**
 * Check if a recording can be transcribed (has audio, API key exists).
 */
function canTranscribe(recordingId) {
  if (!hasApiKey()) return { can: false, reason: 'AssemblyAI API key not configured' };
  const db = brain.getDb();
  const rec = db.prepare('SELECT audio_path, status FROM recordings WHERE id = ?').get(recordingId);
  if (!rec) return { can: false, reason: 'Recording not found' };
  if (!rec.audio_path) return { can: false, reason: 'No audio file' };
  if (rec.status === 'transcribing') return { can: false, reason: 'Already transcribing' };
  return { can: true };
}

function _sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

module.exports = {
  transcribeRecording,
  canTranscribe,
  hasApiKey,
  setApiKey,
  getProgress
};
