// Text-to-speech domain layer.
//
// Providers: 'openai' (default — uses `tts-1` / `tts-1-hd`), 'elevenlabs',
// and 'local' (env-driven HTTP slot for self-hosted engines such as MisoTTS).
// Returns the raw audio buffer plus mime metadata. When `save_to_disk` is
// requested AND a project_id is supplied, writes an artifact under
// data/disk/<project>/voice/ mirroring the transcribeFile pattern.
//
// All HTTP traffic flows through `wrapFetch` for egress logging. Vault keys
// first, env vars as a migration-window fallback. No API key ever lands in
// the returned shape, the audit logs, or any error message.

import fs from 'node:fs';
import { getDb, nowIso, newId } from './db.js';
import { getSecret } from './secrets.js';
import { wrapFetch } from './egress.js';
import { uploadFile, getRootFolder, createFolder } from './disk.js';
import { getProject } from './projects.js';

export const TTS_PROVIDERS = ['openai', 'elevenlabs', 'local'];
export const DEFAULT_OPENAI_TTS_MODEL = 'tts-1';
export const DEFAULT_OPENAI_VOICE = 'alloy';
export const DEFAULT_ELEVENLABS_MODEL = 'eleven_multilingual_v2';
export const DEFAULT_ELEVENLABS_VOICE_ID = '21m00Tcm4TlvDq8ikWAM'; // Rachel — public stock voice

const OPENAI_VOICES = new Set(['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']);

function localTtsUrl() {
  const value = process.env.MADARX_LOCAL_TTS_URL;
  return value && /^https?:\/\//i.test(value.trim()) ? value.trim() : null;
}

function ensureVoiceFolder(projectId) {
  const db = getDb();
  const root = getRootFolder(projectId);
  const existing = db
    .prepare('SELECT * FROM disk_folders WHERE project_id = ? AND parent_id = ? AND name = ? LIMIT 1')
    .get(projectId, root.id, 'voice');
  if (existing) return existing;
  return createFolder(projectId, root.id, 'voice');
}

function pickOpenAiVoice(voice) {
  if (!voice) return DEFAULT_OPENAI_VOICE;
  return OPENAI_VOICES.has(voice) ? voice : DEFAULT_OPENAI_VOICE;
}

function maskMessage(message, ...secrets) {
  let out = String(message ?? '');
  for (const secret of secrets) {
    if (!secret) continue;
    const tail = String(secret).slice(-4);
    out = out.split(String(secret)).join(`***${tail}`);
  }
  return out;
}

async function synthesizeOpenAi({ text, voice, model, format, fetchFn }) {
  const apiKey = getSecret('OPENAI_API_KEY') || process.env.OPENAI_API_KEY;
  if (!apiKey) throw new Error('OPENAI_API_KEY missing for OpenAI TTS');
  const useVoice = pickOpenAiVoice(voice);
  const useModel = model || DEFAULT_OPENAI_TTS_MODEL;
  const useFormat = format || 'mp3';

  let res;
  try {
    res = await fetchFn('https://api.openai.com/v1/audio/speech', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: useModel,
        voice: useVoice,
        input: text,
        format: useFormat,
      }),
    });
  } catch (err) {
    throw new Error(`openai tts failed: ${maskMessage(err?.message, apiKey)}`);
  }
  if (!res.ok) {
    let detail = '';
    try { detail = (await res.text()).slice(0, 256); } catch {}
    throw new Error(`openai tts http ${res.status}: ${maskMessage(detail, apiKey)}`);
  }
  const buffer = Buffer.from(await res.arrayBuffer());
  return {
    audio_buffer: buffer,
    mime_type: useFormat === 'wav' ? 'audio/wav' : 'audio/mpeg',
    extension: useFormat === 'wav' ? 'wav' : 'mp3',
    provider: 'openai',
    voice: useVoice,
    model: useModel,
  };
}

async function synthesizeElevenLabs({ text, voice, model, format, fetchFn }) {
  const apiKey = getSecret('ELEVENLABS_API_KEY') || process.env.ELEVENLABS_API_KEY;
  if (!apiKey) throw new Error('ELEVENLABS_API_KEY missing for ElevenLabs TTS');
  const voiceId = voice || getSecret('ELEVENLABS_VOICE_ID') || process.env.ELEVENLABS_VOICE_ID || DEFAULT_ELEVENLABS_VOICE_ID;
  const useModel = model || DEFAULT_ELEVENLABS_MODEL;
  const outputFormat = format === 'wav' ? 'pcm_16000' : 'mp3_44100_128';

  let res;
  try {
    res = await fetchFn(`https://api.elevenlabs.io/v1/text-to-speech/${encodeURIComponent(voiceId)}?output_format=${outputFormat}`, {
      method: 'POST',
      headers: {
        'xi-api-key': apiKey,
        'Content-Type': 'application/json',
        Accept: 'audio/mpeg',
      },
      body: JSON.stringify({
        text,
        model_id: useModel,
      }),
    });
  } catch (err) {
    throw new Error(`elevenlabs tts failed: ${maskMessage(err?.message, apiKey)}`);
  }
  if (!res.ok) {
    let detail = '';
    try { detail = (await res.text()).slice(0, 256); } catch {}
    throw new Error(`elevenlabs tts http ${res.status}: ${maskMessage(detail, apiKey)}`);
  }
  const buffer = Buffer.from(await res.arrayBuffer());
  return {
    audio_buffer: buffer,
    mime_type: outputFormat.startsWith('pcm_') ? 'audio/wav' : 'audio/mpeg',
    extension: outputFormat.startsWith('pcm_') ? 'wav' : 'mp3',
    provider: 'elevenlabs',
    voice: voiceId,
    model: useModel,
  };
}

async function synthesizeLocal({ text, voice, model, format, fetchFn }) {
  const url = localTtsUrl();
  if (!url) throw new Error('MADARX_LOCAL_TTS_URL missing for local TTS');
  const useFormat = format || 'wav';
  const useModel = model || process.env.MADARX_LOCAL_TTS_MODEL || 'local';
  const useVoice = voice || process.env.MADARX_LOCAL_TTS_VOICE || 'default';

  let res;
  try {
    res = await fetchFn(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Accept: useFormat === 'mp3' ? 'audio/mpeg,application/json' : 'audio/wav,audio/*,application/json',
      },
      body: JSON.stringify({
        text,
        voice: useVoice,
        model: useModel,
        format: useFormat,
      }),
    });
  } catch (err) {
    throw new Error(`local tts failed: ${maskMessage(err?.message)}`);
  }
  if (!res.ok) {
    let detail = '';
    try { detail = (await res.text()).slice(0, 256); } catch {}
    throw new Error(`local tts http ${res.status}: ${detail}`);
  }

  const contentType = String(res.headers.get('content-type') || '').toLowerCase();
  if (contentType.includes('application/json')) {
    let payload;
    try { payload = await res.json(); } catch {
      throw new Error('local tts returned invalid JSON');
    }
    const b64 = payload?.audio_base64 || payload?.audio;
    if (!b64) throw new Error('local tts JSON must include audio_base64');
    const mime = payload.mime_type || payload.content_type || (useFormat === 'mp3' ? 'audio/mpeg' : 'audio/wav');
    const ext = payload.extension || (String(mime).includes('mpeg') || String(mime).includes('mp3') ? 'mp3' : 'wav');
    return {
      audio_buffer: Buffer.from(String(b64), 'base64'),
      mime_type: mime,
      extension: ext,
      provider: 'local',
      voice: payload.voice || useVoice,
      model: payload.model || useModel,
    };
  }

  const buffer = Buffer.from(await res.arrayBuffer());
  const isMp3 = contentType.includes('mpeg') || contentType.includes('mp3') || useFormat === 'mp3';
  return {
    audio_buffer: buffer,
    mime_type: contentType || (isMp3 ? 'audio/mpeg' : 'audio/wav'),
    extension: isMp3 ? 'mp3' : 'wav',
    provider: 'local',
    voice: useVoice,
    model: useModel,
  };
}

/**
 * Synthesize speech.
 *
 * @param {{
 *   text: string,
 *   provider?: 'openai' | 'elevenlabs' | 'local',
 *   voice?: string,
 *   model?: string,
 *   format?: 'mp3' | 'wav',
 *   agent_id?: string | null,
 *   project_id?: string | null,
 *   save_to_disk?: boolean,
 *   fetch?: typeof fetch,
 * }} opts
 * @returns {Promise<{
 *   audio_buffer: Buffer, mime_type: string, extension: string,
 *   provider: string, voice: string, model: string,
 *   duration_estimate_ms: number,
 *   disk_file?: object | null,
 * }>}
 */
export async function synthesizeSpeech(opts = {}) {
  const text = String(opts.text || '').trim();
  if (!text) throw new Error('text is required');
  if (text.length > 4000) throw new Error('text too long: max 4000 characters');

  const providerHint = String(opts.provider || process.env.MADARX_VOICE_DEFAULT_PROVIDER || process.env.TTS_DEFAULT_PROVIDER || 'openai').toLowerCase();
  const provider = TTS_PROVIDERS.includes(providerHint) ? providerHint : 'openai';
  const fetchFn = wrapFetch('voice_speak', opts.agent_id ?? null, opts.fetch || fetch);

  const synth = provider === 'elevenlabs' ? synthesizeElevenLabs : provider === 'local' ? synthesizeLocal : synthesizeOpenAi;
  const result = await synth({ text, voice: opts.voice, model: opts.model, format: opts.format, fetchFn });

  // Rough estimate: ~14 chars per second of speech at default rate.
  const durationMs = Math.max(500, Math.round((text.length / 14) * 1000));

  let diskFile = null;
  if (opts.save_to_disk && opts.project_id) {
    const project = getProject(opts.project_id);
    if (project) {
      const folder = ensureVoiceFolder(project.id);
      const sanitized = text.slice(0, 40).replace(/[^A-Za-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'tts';
      diskFile = uploadFile(project.id, folder.id, {
        name: `${sanitized}-${Date.now()}.${result.extension}`,
        buffer: result.audio_buffer,
        type: result.mime_type,
      }, { local_only: false });
    }
  }

  // Audit row. NEVER include the audio buffer or the API key in the meta.
  try {
    getDb().prepare(`
      INSERT INTO audit_logs (id, at, type, summary, status, meta)
      VALUES (?, ?, ?, ?, ?, ?)
    `).run(
      newId('audit'),
      nowIso(),
      'voice.tts',
      `TTS ${provider} voice=${result.voice} chars=${text.length}`,
      'complete',
      JSON.stringify({
        provider,
        voice: result.voice,
        model: result.model,
        text_chars: text.length,
        audio_bytes: result.audio_buffer.length,
        disk_file_id: diskFile?.id || null,
        agent_id: opts.agent_id || null,
      }),
    );
  } catch (err) {
    console.warn('[tts] audit write failed:', err?.message || err);
  }

  return {
    audio_buffer: result.audio_buffer,
    mime_type: result.mime_type,
    extension: result.extension,
    provider,
    voice: result.voice,
    model: result.model,
    duration_estimate_ms: durationMs,
    disk_file: diskFile,
  };
}

export function ttsProvidersConfigured() {
  return {
    openai: Boolean(getSecret('OPENAI_API_KEY') || process.env.OPENAI_API_KEY),
    elevenlabs: Boolean(getSecret('ELEVENLABS_API_KEY') || process.env.ELEVENLABS_API_KEY),
    local: Boolean(localTtsUrl()),
  };
}
