/**
 * jambonz call status hook — receives ringing/answered/completed events.
 * Updates the matching sip_call_sessions row.
 *
 * Gating mirrors /api/webhooks/sip/voice:
 *   1. Platform-level FEATURE_SIP_ENABLED → 404 if off.
 *   2. HMAC signature against JAMBONZ_WEBHOOK_SECRET (permissive in dev).
 *   3. Restaurant-level `restaurants.sip_enabled` — terminal updates are
 *      still applied so we don't leak permanently-active sessions if the
 *      flag is flipped off mid-call, but ringing/active transitions for a
 *      disabled tenant are rejected.
 *
 * On terminal events the recording_url (if jambonz attached one) is
 * persisted onto sip_call_sessions.recording_url.
 */
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 { childLogger } from '@server/logger';
import { wrapRouteHandler } from '@server/logger/request';
const log = childLogger('webhook.sip.status');

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;
  }
}

const TERMINAL = new Set(['completed', 'failed', 'busy', 'no-answer', 'canceled']);

function pickRecordingUrl(payload: Record<string, unknown>): string | null {
  // jambonz uses a few field names depending on configuration; accept any.
  const recording = payload.recording as { url?: unknown } | undefined;
  const candidates: unknown[] = [
    payload.recording_url,
    payload.recordingUrl,
    recording?.url,
  ];
  for (const c of candidates) {
    if (typeof c === 'string' && c.trim()) return c;
  }
  return null;
}

async function handleSipStatus(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');

  // Peek at the body to find the tenant before we can pick the right
  // HMAC secret. If lookup fails we fall back to the platform env secret;
  // if both are missing the verifier stays permissive in dev only.
  let payload: Record<string, unknown> = {};
  try { payload = rawBody ? JSON.parse(rawBody) : {}; } catch { /* allow */ }

  const callSid = (payload.call_sid as string) || (payload.callSid as string) || '';
  const status = String(payload.call_status || payload.callStatus || '').toLowerCase();
  const duration = Number(payload.call_duration || payload.callDuration || 0) || 0;

  let session: { id: string; restaurant_id: string } | undefined;
  if (callSid) {
    /* raw: SELECT id, restaurant_id FROM sip_call_sessions WHERE sip_call_id = $1 LIMIT 1 */
    const { rows: sRows } = await db.execute(sql`
      SELECT id, restaurant_id FROM sip_call_sessions WHERE sip_call_id = ${callSid} LIMIT 1
    `);
    session = sRows[0] as { id: string; restaurant_id: string } | undefined;
  }

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

  if (!callSid) return NextResponse.json({ received: true });
  if (!session) return NextResponse.json({ received: true });

  /* raw: SELECT sip_enabled FROM restaurants WHERE id = $1 */
  const { rows: rRows } = await db.execute(sql`SELECT sip_enabled FROM restaurants WHERE id = ${session.restaurant_id}`);
  const tenantEnabled = !!(rRows[0] as { sip_enabled?: boolean } | undefined)?.sip_enabled;
  const isTerminal = TERMINAL.has(status);

  // For an active tenant: handle every event normally.
  // For a tenant whose SIP add-on was just disabled: still let TERMINAL
  // events through so we never leave sessions stuck in 'active' forever,
  // but reject mid-call state changes (ringing/active) outright.
  if (!tenantEnabled && !isTerminal) {
    return NextResponse.json({ error: 'SIP not enabled' }, { status: 403 });
  }

  if (isTerminal) {
    const dbStatus = status === 'completed' ? 'completed' : 'failed';
    const recordingUrl = pickRecordingUrl(payload);
    /* raw: UPDATE sip_call_sessions SET status, ended_at, duration_seconds, recording_url=COALESCE($,recording_url) WHERE sip_call_id=$ AND status IN ('active','ringing') */
    await db.execute(sql`UPDATE sip_call_sessions
      SET status = ${dbStatus},
          ended_at = NOW(),
          duration_seconds = ${duration > 0 ? duration : sql`EXTRACT(EPOCH FROM (NOW() - started_at))::INTEGER`},
          recording_url = COALESCE(${recordingUrl}, recording_url)
      WHERE sip_call_id = ${callSid} AND status IN ('active','ringing')`);
  } else if (status === 'ringing' || status === 'in-progress' || status === 'answered') {
    await db.execute(sql`UPDATE sip_call_sessions SET status = 'active' WHERE sip_call_id = ${callSid} AND status = 'ringing'`);
  }

  return NextResponse.json({ received: true });
}

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