/**
 * jambonz call hook — invoked when a call hits a SIP-routed number.
 * Validates the HMAC signature (JAMBONZ_WEBHOOK_SECRET), looks up the phone
 * number, creates a sip_call_sessions row, and returns a jambonz application
 * JSON that opens an audio WebSocket back to /sip-stream/:sessionId.
 */
import { NextRequest, NextResponse } from 'next/server';
import { createHmac, timingSafeEqual } from 'crypto';
import { initDatabase } from '@server/db/init';
import { db } from '@server/db/drizzle';
import { sql } from 'drizzle-orm';
import { isSipFeatureEnabled } from '@/shared/config/sip-feature';
import { getJambonzWebhookSecret } from '@server/services/telephone/jambonz/jambonz.service';
import { getPlanLimits } from '@server/utils/features';

import { childLogger } from '@server/logger';
import { wrapRouteHandler } from '@server/logger/request';
const log = childLogger('webhook.sip.voice');

/**
 * Verify the HMAC signature against the per-tenant secret (resolved by
 * `getJambonzWebhookSecret`) with platform env fallback. When neither is
 * configured we stay permissive in dev only.
 */
function verifySignature(rawBody: string, header: string | null, secret: string | null): boolean {
  if (!secret) return process.env.NODE_ENV !== 'production';
  if (!header) return false;
  const computed = createHmac('sha256', secret).update(rawBody).digest('hex');
  try {
    const a = Buffer.from(computed, 'hex');
    const b = Buffer.from(header.replace(/^sha256=/, ''), 'hex');
    return a.length === b.length && timingSafeEqual(a, b);
  } catch {
    return false;
  }
}

function getPublicBaseUrl(req: NextRequest): string {
  const env = process.env.WEBHOOK_BASE_URL || process.env.NEXT_PUBLIC_APP_URL;
  if (env) return env.replace(/\/+$/, '');
  const host = req.headers.get('host') || 'localhost:5000';
  return `https://${host}`;
}

async function handleSipVoice(req: NextRequest) {
  if (!isSipFeatureEnabled()) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 });
  }
  await initDatabase();

  const rawBody = await req.text();
  const signature = req.headers.get('x-jambonz-signature') || req.headers.get('jambonz-signature');

  // Two-pass auth: parse the body untrusted to determine the candidate
  // tenant, resolve their per-restaurant webhook secret (env fallback),
  // then verify the HMAC. We can't trust the payload until the signature
  // passes — but we also can't pick a tenant secret without first
  // peeking at the routing fields.
  let payload: Record<string, unknown> = {};
  try { payload = rawBody ? JSON.parse(rawBody) : {}; } catch { /* allow empty */ }

  const callSid = (payload.call_sid as string) || (payload.callSid as string) || '';
  const from = (payload.from as string) || '';
  const to = (payload.to as string) || '';
  const url = new URL(req.url);
  const queryParam = url.searchParams.get('sessionId');
  const presetSessionId = queryParam || (payload.custom_call_data as { sessionId?: string } | undefined)?.sessionId || null;

  let candidateRestaurantId: string | null = null;
  if (presetSessionId) {
    const { rows } = await db.execute(sql`SELECT restaurant_id FROM sip_call_sessions WHERE id = ${presetSessionId} LIMIT 1`);
    candidateRestaurantId = (rows[0] as { restaurant_id?: string } | undefined)?.restaurant_id || null;
  } else if (to) {
    const { rows } = await db.execute(sql`SELECT restaurant_id FROM phone_numbers WHERE provider = 'sip' AND is_active = true AND number = ${to} LIMIT 1`);
    candidateRestaurantId = (rows[0] as { restaurant_id?: string } | undefined)?.restaurant_id || null;
  }

  const tenantSecret = candidateRestaurantId ? await getJambonzWebhookSecret(candidateRestaurantId) : (process.env.JAMBONZ_WEBHOOK_SECRET || null);
  const sigOk = verifySignature(rawBody, signature, tenantSecret);
  log.info(
    { signatureValid: sigOk, event: 'voice', id: callSid || null, type: 'webhook.received' },
    'webhook.received'
  );
  if (!sigOk) {
    return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
  }

  let sessionId: string;
  let restaurantId: string;

  if (presetSessionId) {
    /* raw: SELECT id, restaurant_id FROM sip_call_sessions WHERE id = $1 */
    const { rows: sRows } = await db.execute(sql`SELECT id, restaurant_id FROM sip_call_sessions WHERE id = ${presetSessionId} LIMIT 1`);
    const existing = sRows[0] as { id: string; restaurant_id: string } | undefined;
    if (!existing) {
      return NextResponse.json([{ verb: 'say', text: 'Session not found.' }, { verb: 'hangup' }]);
    }
    /* raw: SELECT sip_enabled FROM restaurants WHERE id = $1 */
    const { rows: rRows } = await db.execute(sql`SELECT sip_enabled FROM restaurants WHERE id = ${existing.restaurant_id}`);
    if (!(rRows[0] as { sip_enabled?: boolean } | undefined)?.sip_enabled) {
      return NextResponse.json([{ verb: 'say', text: 'SIP service not enabled for this restaurant.' }, { verb: 'hangup' }]);
    }
    sessionId = existing.id;
    restaurantId = existing.restaurant_id;
    if (callSid) {
      /* raw: UPDATE sip_call_sessions SET sip_call_id = COALESCE(sip_call_id, $1), status = 'active' WHERE id = $2 */
      await db.execute(sql`UPDATE sip_call_sessions SET sip_call_id = COALESCE(sip_call_id, ${callSid}), status = 'active' WHERE id = ${sessionId}`);
    }
  } else {
    /* raw: SELECT id, restaurant_id, assigned_agent_id FROM phone_numbers WHERE provider = 'sip' AND is_active = true AND number = $1 LIMIT 1 */
    const { rows: phoneRows } = await db.execute(sql`
      SELECT id, restaurant_id, assigned_agent_id FROM phone_numbers
      WHERE provider = 'sip' AND is_active = true AND number = ${to}
      LIMIT 1
    `);
    const phone = phoneRows[0] as { id: string; restaurant_id: string; assigned_agent_id: string | null } | undefined;
    if (!phone) {
      return NextResponse.json([{ verb: 'say', text: 'This number is not configured.' }, { verb: 'hangup' }]);
    }

    /* raw: SELECT sip_enabled FROM restaurants WHERE id = $1 */
    const { rows: rRows } = await db.execute(sql`SELECT sip_enabled FROM restaurants WHERE id = ${phone.restaurant_id}`);
    if (!(rRows[0] as { sip_enabled?: boolean } | undefined)?.sip_enabled) {
      return NextResponse.json([{ verb: 'say', text: 'SIP service not enabled for this restaurant.' }, { verb: 'hangup' }]);
    }

    // Enforce concurrent_voice_calls plan limit before creating an inbound SIP session
    try {
      const limits = await getPlanLimits(phone.restaurant_id);
      if (limits.concurrent_voice_calls !== null) {
        const { rows: activeRows } = await db.execute(sql`
          SELECT COUNT(*) AS count FROM sip_call_sessions
          WHERE restaurant_id = ${phone.restaurant_id} AND status = 'active'
        `);
        const active = parseInt((activeRows[0] as { count: string })?.count ?? '0', 10);
        if (active >= limits.concurrent_voice_calls) {
          log.warn({ restaurantId: phone.restaurant_id, active, limit: limits.concurrent_voice_calls }, 'concurrent_voice_calls limit reached for inbound SIP call');
          return NextResponse.json([{ verb: 'say', text: 'We are currently at maximum call capacity. Please try again later.' }, { verb: 'hangup' }]);
        }
      }
    } catch (err) {
      log.warn({ err }, 'failed to check concurrent_voice_calls limit; allowing SIP call');
    }

    const { rows: sessionRows } = await db.execute(sql`
      INSERT INTO sip_call_sessions (restaurant_id, phone_number_id, agent_id, caller_number, callee_number, direction, status, sip_call_id, transcript)
      VALUES (${phone.restaurant_id}, ${phone.id}, ${phone.assigned_agent_id || null}, ${from}, ${to}, 'inbound', 'active', ${callSid || null}, '[]'::jsonb)
      RETURNING id
    `);
    sessionId = (sessionRows[0] as { id: string }).id;
    restaurantId = phone.restaurant_id;
  }
  void restaurantId;

  const baseUrl = getPublicBaseUrl(req);
  const wsBase = baseUrl.replace(/^http/, 'ws');

  // jambonz "verb" response — connect call audio to our /sip-stream WS.
  //
  // Contract notes:
  //   • mixType=mono → single PCM channel (matches OpenAI Realtime pcm16
  //     input/output; stereo would interleave caller/callee and break the
  //     OpenAI transport which expects mono frames).
  //   • sampleRate=24000 → matches OpenAI Realtime pcm16 sample rate so we
  //     can forward jambonz frames straight through without resampling.
  //   • bidirectionalAudio.streaming=true → jambonz pushes caller audio
  //     and accepts agent audio over the same socket (full-duplex).
  return NextResponse.json([
    {
      verb: 'listen',
      url: `${wsBase}/sip-stream/${sessionId}`,
      mixType: 'mono',
      sampleRate: 24000,
      passDtmf: true,
      bidirectionalAudio: { enabled: true, streaming: true, sampleRate: 24000 },
    },
  ]);
}

export const POST = wrapRouteHandler((req: Request) => handleSipVoice(req as NextRequest));
