import { WebSocket } from 'ws';
import { db } from '@server/db/drizzle';
import { sql } from 'drizzle-orm';
import { RealtimeAgent, RealtimeSession } from '@openai/agents/realtime';
import { TwilioRealtimeTransportLayer } from '@openai/agents-extensions';
import { AgentConfig } from '../types';
import { makeRestaurantTools } from './tools';

const DEFAULT_REALTIME_MODEL = 'gpt-realtime-1.5';
const DEFAULT_VOICE = 'alloy';

const DEFAULT_SYSTEM_PROMPT = `You are a friendly and professional AI phone agent for a restaurant. Your job is to assist callers efficiently and warmly over the phone.

Guidelines:
- Greet callers warmly and make them feel welcome.
- Answer questions about the restaurant using your knowledge base search tool whenever possible.
- Keep responses concise — callers are on the phone, so avoid long paragraphs.
- Speak naturally and confirm back important details before finalising.
- Do not make up menu items, prices, or hours you are not certain about.
- Always be polite, patient, and professional.`;

export async function getOpenAIKey(restaurantId: string): Promise<string | null> {
  try {
    /* raw: SELECT encrypted_api_key FROM restaurant_api_keys WHERE restaurant_id = $1 AND provider_name = 'openai' AND is_active = true LIMIT 1 */
    const row = await db.execute(sql`SELECT encrypted_api_key FROM restaurant_api_keys
       WHERE restaurant_id = ${restaurantId} AND provider_name = 'openai' AND is_active = true
       LIMIT 1`);
    if (row.rows[0]?.encrypted_api_key) {
      const key = row.rows[0].encrypted_api_key as string;
      if (key.startsWith('sk-')) return key;
      try {
        const { decrypt } = await import('@server/utils/crypto');
        return decrypt(key);
      } catch {
        return key;
      }
    }
  } catch {
    /* fall through to env */
  }
  return process.env.OPENAI_API_KEY || null;
}

function mapAgentRow(r: Record<string, unknown>): AgentConfig {
  return {
    session_id: r.session_id as string,
    restaurant_id: r.restaurant_id as string,
    agent_id: (r.agent_id as string) || null,
    system_prompt: (r.system_prompt as string) || null,
    greeting_script: (r.greeting_script as string) || null,
    closing_script: (r.closing_script as string) || null,
    voice_id: (r.voice_id as string) || DEFAULT_VOICE,
    realtime_model: (r.realtime_model as string) || DEFAULT_REALTIME_MODEL,
    fallback_rules: (r.fallback_rules as AgentConfig['fallback_rules']) || null,
    capabilities: (r.capabilities as AgentConfig['capabilities']) || null,
    vad_threshold: r.vad_threshold != null ? Number(r.vad_threshold) : null,
    vad_prefix_padding_ms: r.vad_prefix_padding_ms != null ? Number(r.vad_prefix_padding_ms) : null,
    vad_silence_duration_ms: r.vad_silence_duration_ms != null ? Number(r.vad_silence_duration_ms) : null,
  };
}

export async function lookupAgentByCallSid(callSid: string): Promise<AgentConfig | null> {
  try {
    /* raw: SELECT s.id AS session_id, s.restaurant_id, s.agent_id, a.system_prompt, a.greeting_script, a.closing_script, a.fallback_rules, a.capabilities, a.realtime_model, a.vad_threshold, a.vad_prefix_padding_ms, a.vad_silence_duration_ms, COALESCE(vm.voice_id, $2) AS voice_id FROM sip_call_sessions s LEFT JOIN ai_agents a ON a.id = s.agent_id LEFT JOIN voice_models vm ON vm.id = a.voice_model_id WHERE s.sip_call_id = $1 LIMIT 1 */
    const result = await db.execute(sql`SELECT s.id AS session_id, s.restaurant_id, s.agent_id,
        a.system_prompt, a.greeting_script, a.closing_script,
        a.fallback_rules, a.capabilities, a.realtime_model,
        a.vad_threshold, a.vad_prefix_padding_ms, a.vad_silence_duration_ms,
        COALESCE(vm.voice_id, ${DEFAULT_VOICE}) AS voice_id
      FROM sip_call_sessions s
      LEFT JOIN ai_agents a ON a.id = s.agent_id
      LEFT JOIN voice_models vm ON vm.id = a.voice_model_id
      WHERE s.sip_call_id = ${callSid} LIMIT 1`);
    if (!result.rows[0]) return null;
    return mapAgentRow(result.rows[0] as Record<string, unknown>);
  } catch (e) {
    console.error('[Engine/Twilio] DB lookup error:', e);
    return null;
  }
}

export async function lookupAgentBySessionId(sessionId: string): Promise<AgentConfig | null> {
  try {
    /* raw: SELECT s.id AS session_id, s.restaurant_id, s.agent_id, a.system_prompt, a.greeting_script, a.closing_script, a.fallback_rules, a.capabilities, a.realtime_model, a.vad_threshold, a.vad_prefix_padding_ms, a.vad_silence_duration_ms, COALESCE(vm.voice_id, $2) AS voice_id FROM sip_call_sessions s LEFT JOIN ai_agents a ON a.id = s.agent_id LEFT JOIN voice_models vm ON vm.id = a.voice_model_id WHERE s.id = $1 LIMIT 1 */
    const result = await db.execute(sql`SELECT s.id AS session_id, s.restaurant_id, s.agent_id,
        a.system_prompt, a.greeting_script, a.closing_script,
        a.fallback_rules, a.capabilities, a.realtime_model,
        a.vad_threshold, a.vad_prefix_padding_ms, a.vad_silence_duration_ms,
        COALESCE(vm.voice_id, ${DEFAULT_VOICE}) AS voice_id
      FROM sip_call_sessions s
      LEFT JOIN ai_agents a ON a.id = s.agent_id
      LEFT JOIN voice_models vm ON vm.id = a.voice_model_id
      WHERE s.id = ${sessionId} LIMIT 1`);
    if (!result.rows[0]) return null;
    return mapAgentRow(result.rows[0] as Record<string, unknown>);
  } catch (e) {
    console.error('[Engine/Twilio] DB lookup error:', e);
    return null;
  }
}

interface BranchContext {
  name: string;
  address: string | null;
  phone: string | null;
  hours: Record<string, { open: string; close: string; closed?: boolean }> | null;
  timezone: string | null;
}

const DAY_ORDER = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday'];
const DAY_LABELS: Record<string, string> = {
  monday: 'Mon', tuesday: 'Tue', wednesday: 'Wed',
  thursday: 'Thu', friday: 'Fri', saturday: 'Sat', sunday: 'Sun',
};

function formatBranchHours(ctx: BranchContext): string | null {
  const h = ctx.hours;
  if (!h || typeof h !== 'object') return null;
  const lines: string[] = [];
  for (const day of DAY_ORDER) {
    const slot = h[day];
    if (!slot) continue;
    lines.push(slot.closed ? `${DAY_LABELS[day]}: Closed` : (slot.open && slot.close ? `${DAY_LABELS[day]}: ${slot.open}–${slot.close}` : ''));
  }
  const filtered = lines.filter(Boolean);
  if (filtered.length === 0) return null;
  const tzNote = ctx.timezone ? ` (${ctx.timezone})` : '';
  return `Working hours${tzNote}: ${filtered.join(', ')}`;
}

async function fetchBranchContext(sessionId: string): Promise<BranchContext | null> {
  try {
    /* raw: SELECT b.name, b.address, b.phone, b.hours, b.timezone FROM sip_call_sessions s JOIN phone_numbers pn ON pn.id = s.phone_number_id JOIN branches b ON b.id = pn.branch_id WHERE s.id = $1 LIMIT 1 */
    const result = await db.execute(sql`SELECT b.name, b.address, b.phone, b.hours, b.timezone
       FROM sip_call_sessions s
       JOIN phone_numbers pn ON pn.id = s.phone_number_id
       JOIN branches b ON b.id = pn.branch_id
       WHERE s.id = ${sessionId} LIMIT 1`);
    if (!result.rows[0]) return null;
    return result.rows[0] as unknown as BranchContext;
  } catch {
    return null;
  }
}

function buildInstructions(agent: AgentConfig, branch?: BranchContext | null): string {
  const parts = [agent.system_prompt?.trim() || DEFAULT_SYSTEM_PROMPT];

  const caps = agent.capabilities || {};

  const enabledCapLabels: Record<string, string> = {
    order_taking: 'taking food orders',
    reservation_booking: 'booking table reservations',
    escalation: 'escalating to a human agent when needed',
  };
  const enabledCaps = Object.entries(caps)
    .filter(([, v]) => v)
    .map(([k]) => enabledCapLabels[k] || k.replace(/_/g, ' '));
  if (enabledCaps.length > 0) {
    parts.push(`\n\nYou are enabled to assist with: ${enabledCaps.join(', ')}.`);
  }

  const rules = (agent.fallback_rules || []).filter(r => r.trigger?.trim() && r.action?.trim());
  if (rules.length > 0) {
    const sorted = [...rules].sort((a, b) => {
      const ord: Record<string, number> = { high: 0, medium: 1, low: 2 };
      return (ord[a.priority || 'medium'] ?? 1) - (ord[b.priority || 'medium'] ?? 1);
    });
    parts.push(`\n\nFallback rules:\n${sorted.map(r => `- If ${r.trigger}: ${r.action}`).join('\n')}`);
  }

  if (branch) {
    const contactParts: string[] = [`Branch: ${branch.name}`];
    if (branch.phone) contactParts.push(`Phone: ${branch.phone}`);
    if (branch.address) contactParts.push(`Address: ${branch.address}`);
    const hoursStr = formatBranchHours(branch);
    if (hoursStr) contactParts.push(hoursStr);
    parts.push(`\n\nRestaurant contact information: ${contactParts.join(' | ')}`);
  }

  parts.push('\n\nWhen callers ask about opening hours, policies, menu details, FAQs, or any restaurant-specific information, use the search_knowledge_base tool to find accurate answers.');

  if (caps.order_taking) {
    parts.push('\n\nWhen a caller wants to place a food order: collect all items and quantities, ask for their name, phone number, and whether it is dine-in, takeaway, or delivery. Confirm the full order back to them, then call the place_order tool. Do not call place_order until the caller has confirmed.');
  }

  if (caps.reservation_booking) {
    parts.push('\n\nWhen a caller wants to book a table: collect their name, phone number, party size, preferred date, and time. Confirm the details back to them, then call the book_table tool. Do not call book_table until the caller has confirmed.');
  }

  if (caps.escalation) {
    parts.push('\n\nIf a caller asks to speak to a person, a manager, or a human, or if you cannot resolve their query, immediately call the escalate_call tool with the reason. Do not keep trying to help after the caller has asked for a human.');
  }

  const greeting = agent.greeting_script?.trim();
  if (greeting) {
    parts.push(`\n\nIMPORTANT: Begin the conversation by saying exactly: "${greeting}"`);
  }

  return parts.join('');
}

interface TranscriptEntry {
  role: 'user' | 'assistant';
  text: string;
  ts: string;
}

async function saveTranscript(sessionId: string, entries: TranscriptEntry[]): Promise<void> {
  if (entries.length === 0) return;
  try {
    /* raw: UPDATE sip_call_sessions SET transcript = $1::jsonb WHERE id = $2 */
    await db.execute(sql`UPDATE sip_call_sessions SET transcript = ${JSON.stringify(entries)}::jsonb WHERE id = ${sessionId}`);
  } catch (e) {
    console.warn('[Engine/Twilio] Transcript save error:', e);
  }
}

async function updateSessionStatus(sessionId: string, status: 'completed' | 'failed'): Promise<void> {
  try {
    /* raw: UPDATE sip_call_sessions SET status = $1, ended_at = NOW(), duration_seconds = EXTRACT(EPOCH FROM (NOW() - started_at))::INTEGER WHERE id = $2 AND status = 'active' */
    await db.execute(sql`UPDATE sip_call_sessions
       SET status = ${status}, ended_at = NOW(),
           duration_seconds = EXTRACT(EPOCH FROM (NOW() - started_at))::INTEGER
       WHERE id = ${sessionId} AND status = 'active'`);
  } catch (e) {
    console.warn('[Engine/Twilio] Session status update error:', e);
  }
}

export async function handleTwilioCall(ws: WebSocket, sessionId: string): Promise<void> {
  console.log(`[Engine/Twilio] New connection (sessionId=${sessionId})`);

  const agentConfig = await lookupAgentBySessionId(sessionId);
  if (!agentConfig) {
    console.warn(`[Engine/Twilio] No session found for sessionId=${sessionId}`);
    ws.close();
    return;
  }

  const apiKey = await getOpenAIKey(agentConfig.restaurant_id);
  if (!apiKey) {
    console.error('[Engine/Twilio] No OpenAI API key found');
    ws.close();
    return;
  }

  const transcript: TranscriptEntry[] = [];
  let sessionClosed = false;

  const closeSession = async (status: 'completed' | 'failed' = 'completed') => {
    if (sessionClosed) return;
    sessionClosed = true;
    await saveTranscript(agentConfig.session_id, transcript);
    await updateSessionStatus(agentConfig.session_id, status);
    console.log(`[Engine/Twilio] Session closed (sessionId=${sessionId}, status=${status})`);
  };

  const model = agentConfig.realtime_model || DEFAULT_REALTIME_MODEL;
  const voice = agentConfig.voice_id || DEFAULT_VOICE;

  const branchCtx = await fetchBranchContext(sessionId);

  const transport = new TwilioRealtimeTransportLayer({ twilioWebSocket: ws as unknown as WebSocket });

  const agent = new RealtimeAgent({
    name: 'RestaurantAgent',
    instructions: buildInstructions(agentConfig, branchCtx),
    tools: makeRestaurantTools({
      session_id: agentConfig.session_id,
      restaurant_id: agentConfig.restaurant_id,
      agent_id: agentConfig.agent_id,
      capabilities: agentConfig.capabilities,
    }),
  });

  const vadThreshold = agentConfig.vad_threshold ?? 0.5;
  const vadPrefixPaddingMs = agentConfig.vad_prefix_padding_ms ?? 300;
  const vadSilenceDurationMs = agentConfig.vad_silence_duration_ms ?? 600;

  const session = new RealtimeSession(agent, {
    transport,
    model,
    apiKey,
    tracingDisabled: true,
    config: {
      audio: {
        output: { voice: voice as 'alloy' | 'echo' | 'shimmer' | 'ash' | 'ballad' | 'coral' | 'sage' | 'verse' },
        input: {
          turnDetection: {
            type: 'server_vad',
            threshold: vadThreshold,
            prefixPaddingMs: vadPrefixPaddingMs,
            silenceDurationMs: vadSilenceDurationMs,
            createResponse: true,
          },
        },
      },
    },
  });

  session.on('history_updated', (items) => {
    for (const item of items) {
      if (item.type !== 'message') continue;
      const msgItem = item as { type: 'message'; role: string; content?: Array<{ type: string; transcript?: string; text?: string }> };
      const textContent = (msgItem.content || []).find(c => c.type === 'text' || c.type === 'audio');
      const text = textContent?.transcript || textContent?.text;
      if (!text?.trim()) continue;
      const role = msgItem.role === 'user' ? 'user' : 'assistant';
      if (transcript.length === 0 || transcript[transcript.length - 1].text !== text.trim()) {
        transcript.push({ role, text: text.trim(), ts: new Date().toISOString() });
      }
    }
  });

  ws.on('close', () => {
    closeSession('completed').catch(() => {});
  });

  ws.on('error', (err) => {
    console.error('[Engine/Twilio] WebSocket error:', err.message);
    closeSession('failed').catch(() => {});
  });

  try {
    await session.connect({ apiKey });
    console.log(`[Engine/Twilio] RealtimeSession connected (model=${model}, voice=${voice})`);
  } catch (err) {
    console.error('[Engine/Twilio] Failed to connect RealtimeSession:', err);
    await closeSession('failed');
  }
}
