import { getSecret } from './secrets.js';
import { wrapFetch } from './egress.js';
import { executeRegistryTool } from './tools/registry.js';
import { getDb, newId, nowIso } from './db.js';
import { buildVoiceContext } from './voice/context.js';
import { executeVoiceTool, isVoiceTool, getVoiceToolsForRealtime } from './voice/tools.js';

export const DEFAULT_REALTIME_MODEL = 'gpt-realtime-2';
export const DEFAULT_REALTIME_VOICE = 'alloy';
const OPENAI_REALTIME_CLIENT_SECRETS_URL = 'https://api.openai.com/v1/realtime/client_secrets';
const ALLOWED_VOICES = new Set(['alloy', 'ash', 'ballad', 'coral', 'echo', 'sage', 'shimmer', 'verse', 'marin', 'cedar']);

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

function safeVoice(voice) {
  const value = String(voice || '').trim().toLowerCase();
  return ALLOWED_VOICES.has(value) ? value : DEFAULT_REALTIME_VOICE;
}

export function realtimeConfigured() {
  return Boolean(getSecret('OPENAI_API_KEY') || process.env.OPENAI_API_KEY);
}

export async function mintOpenAIRealtimeSession(opts = {}) {
  const apiKey = getSecret('OPENAI_API_KEY') || process.env.OPENAI_API_KEY;
  if (!apiKey) throw new Error('OPENAI_API_KEY missing for OpenAI Realtime');

  const model = String(opts.model || getSecret('OPENAI_REALTIME_MODEL') || process.env.OPENAI_REALTIME_MODEL || DEFAULT_REALTIME_MODEL);
  const voice = safeVoice(opts.voice || getSecret('OPENAI_REALTIME_VOICE') || process.env.OPENAI_REALTIME_VOICE);

  // Build the durable Madarx instruction block from project + memory + state
  // unless the caller passed an override. Tools default to the curated voice
  // catalog; callers can pass a custom list (e.g. [] to disable tool calling).
  let instructions = String(opts.instructions || '').trim();
  let tools = Array.isArray(opts.tools) ? opts.tools : null;
  if (!instructions || !tools) {
    const ctx = buildVoiceContext({
      project_id: opts.project_id,
      agent_id: opts.agent_id,
      active_space: opts.active_space || null,
      all_spaces: opts.all_spaces || null,
      speaking_style: opts.speaking_style || null,
    });
    if (!instructions) instructions = ctx.instructions;
    if (!tools) tools = getVoiceToolsForRealtime();
  }
  instructions = instructions.slice(0, 8000);

  const fetchFn = wrapFetch('openai_realtime_session', opts.agent_id ?? null, opts.fetch || fetch);

  // GA shape: POST /v1/realtime/client_secrets with a nested `session` object
  // carrying type:'realtime' + model + audio + tools. The flat `voice` /
  // `input_audio_format` / `modalities` fields from the Beta endpoint are gone.
  // Audio format is negotiated by WebRTC SDP, so we only set the output voice.
  // Ref: https://platform.openai.com/docs/api-reference/realtime-sessions/create-realtime-client-secret
  const session = {
    type: 'realtime',
    model,
    instructions,
    audio: { output: { voice } },
  };
  if (Array.isArray(tools) && tools.length) session.tools = tools;
  const body = { session };

  let res;
  try {
    res = await fetchFn(OPENAI_REALTIME_CLIENT_SECRETS_URL, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(body),
    });
  } catch (err) {
    throw new Error(`openai realtime session failed: ${mask(err?.message, apiKey)}`);
  }
  if (!res.ok) {
    let detail = '';
    try { detail = (await res.text()).slice(0, 500); } catch {}
    throw new Error(`openai realtime session http ${res.status}: ${mask(detail, apiKey)}`);
  }

  const json = await res.json();
  const clientSecret = json?.client_secret?.value || json?.client_secret || json?.value;
  const expiresAt = json?.client_secret?.expires_at || json?.expires_at || null;
  const sessionId = json?.id || newId('voice_session');

  try {
    getDb().prepare(`
      INSERT OR REPLACE INTO voice_sessions (id, kind, project_id, agent_id, started_at, provider, model, voice, transcript_text, error)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      sessionId,
      'realtime',
      opts.project_id || null,
      opts.agent_id || 'main',
      nowIso(),
      'openai',
      model,
      voice,
      '',
      null,
    );
  } catch {
    // Observation-only table; never block voice setup because logging failed.
  }

  return {
    id: sessionId,
    model,
    voice,
    provider: 'openai',
    client_secret: clientSecret,
    expires_at: expiresAt,
  };
}

export async function relayRealtimeToolCall(opts = {}) {
  const sessionId = String(opts.session_id || '').trim();
  const callId = String(opts.call_id || newId('voice_call'));
  const name = String(opts.name || '').trim();
  if (!name) throw new Error('tool name is required');
  const args = opts.args && typeof opts.args === 'object' ? opts.args : {};
  const started = Date.now();

  // Voice catalog (create_artifact / update_artifact / delete_artifact /
  // find_artifacts / get_state / web_search / web_fetch / voice_speak) flows
  // through executeTool so orchestrator-only tools work. Anything outside the
  // catalog (e.g. an explicitly-requested marketplace tool name) still routes
  // through executeRegistryTool — agent_id 'main' so the main-agent gates fire
  // when there is no installed agent persona on the call.
  const ctx = { project_id: opts.project_id || null, agent_id: opts.agent_id || 'main' };
  let result;
  if (isVoiceTool(name)) {
    result = await executeVoiceTool(name, args, ctx);
  } else {
    result = await executeRegistryTool(name, args, { agent_id: opts.agent_id || 'main' });
  }
  const latencyMs = Date.now() - started;

  try {
    getDb().prepare(`
      INSERT INTO voice_tool_calls (id, session_id, call_id, tool_name, args_summary, status, latency_ms, created_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    `).run(
      newId('voice_tool_call'),
      sessionId || null,
      callId,
      name,
      JSON.stringify(args).slice(0, 2000),
      result?.status || (result?.ok ? 'complete' : 'failed'),
      latencyMs,
      nowIso(),
    );
  } catch {
    // Observation-only.
  }

  return {
    ok: result?.ok !== false,
    call_id: callId,
    tool_name: name,
    status: result?.status || (result?.ok === false ? 'failed' : 'complete'),
    latency_ms: latencyMs,
    result,
  };
}
