/**
 * jambonz.service.ts — minimal HTTP REST client for jambonz.
 * Wraps carrier (SIP gateway), application, and call APIs.
 *
 * Per-restaurant credentials live in `restaurant_api_keys` (provider_name='jambonz')
 * with platform env fallback (JAMBONZ_BASE_URL / JAMBONZ_API_KEY / JAMBONZ_ACCOUNT_SID).
 */
import { db } from '@server/db/drizzle';
import { sql } from 'drizzle-orm';
import { decrypt } from '@server/utils/crypto';

export interface JambonzCreds {
  baseUrl: string;
  apiKey: string;
  accountSid: string;
}

export type SipTransport = 'UDP' | 'TCP' | 'TLS';

export interface JambonzCarrierInput {
  name: string;
  sip_realm: string;
  username?: string;
  password?: string;
  outbound_proxy?: string;
  is_active?: boolean;
  /** SIP signalling port. Defaults to 5060 (5061 for TLS). */
  port?: number;
  /** SIP signalling transport — controls protocol on the gateway. */
  transport?: SipTransport;
}

function defaultPortFor(transport?: SipTransport): number {
  return transport === 'TLS' ? 5061 : 5060;
}

function jambonzProtocol(transport?: SipTransport): 'udp' | 'tcp' | 'tls' {
  return (transport?.toLowerCase() as 'udp' | 'tcp' | 'tls') || 'udp';
}

export interface JambonzCallInput {
  from: string;
  to: { type: 'phone' | 'sip'; number?: string; sipUri?: string };
  application_sid?: string;
  call_hook: string;
  call_status_hook?: string;
}

async function lookupRestaurantCreds(restaurantId: string): Promise<Partial<JambonzCreds> & { webhookSecret?: string }> {
  try {
    /* raw: SELECT encrypted_api_key, metadata FROM restaurant_api_keys WHERE restaurant_id = $1 AND provider_name = 'jambonz' AND is_active = true LIMIT 1 */
    const { rows } = await db.execute(sql`SELECT encrypted_api_key, metadata FROM restaurant_api_keys
      WHERE restaurant_id = ${restaurantId} AND provider_name = 'jambonz' AND is_active = true LIMIT 1`);
    if (!rows[0]) return {};
    const r = rows[0] as { encrypted_api_key: string; metadata: Record<string, unknown> | null };
    let apiKey: string;
    try { apiKey = decrypt(r.encrypted_api_key); } catch { apiKey = r.encrypted_api_key; }
    const meta = (r.metadata || {}) as { base_url?: string; account_sid?: string; encrypted_webhook_secret?: string };
    let webhookSecret: string | undefined;
    if (meta.encrypted_webhook_secret) {
      try { webhookSecret = decrypt(meta.encrypted_webhook_secret); } catch { webhookSecret = undefined; }
    }
    return {
      apiKey,
      baseUrl: meta.base_url || undefined,
      accountSid: meta.account_sid || undefined,
      webhookSecret,
    };
  } catch {
    return {};
  }
}

export async function getJambonzCreds(restaurantId: string): Promise<JambonzCreds | null> {
  const fromDb = await lookupRestaurantCreds(restaurantId);
  const baseUrl = (fromDb.baseUrl || process.env.JAMBONZ_BASE_URL || '').replace(/\/+$/, '');
  const apiKey = fromDb.apiKey || process.env.JAMBONZ_API_KEY || '';
  const accountSid = fromDb.accountSid || process.env.JAMBONZ_ACCOUNT_SID || '';
  if (!baseUrl || !apiKey || !accountSid) return null;
  return { baseUrl, apiKey, accountSid };
}

/**
 * Resolve the webhook HMAC secret for a tenant. Per-restaurant value
 * (encrypted in `restaurant_api_keys.metadata.encrypted_webhook_secret`)
 * takes precedence; falls back to the platform env var. Returns null
 * when neither is configured.
 */
export async function getJambonzWebhookSecret(restaurantId: string): Promise<string | null> {
  const fromDb = await lookupRestaurantCreds(restaurantId);
  return fromDb.webhookSecret || process.env.JAMBONZ_WEBHOOK_SECRET || null;
}

async function jambonzFetch<T = unknown>(
  creds: JambonzCreds,
  path: string,
  init: RequestInit = {}
): Promise<{ ok: boolean; status: number; data: T | null; error?: string }> {
  const url = `${creds.baseUrl}/v1${path}`;
  try {
    const res = await fetch(url, {
      ...init,
      headers: {
        Authorization: `Bearer ${creds.apiKey}`,
        'Content-Type': 'application/json',
        ...(init.headers || {}),
      },
    });
    const text = await res.text();
    let data: unknown = null;
    try { data = text ? JSON.parse(text) : null; } catch { data = text; }
    if (!res.ok) {
      const errMsg = (data && typeof data === 'object' && 'msg' in data ? (data as { msg: string }).msg : null) || `HTTP ${res.status}`;
      return { ok: false, status: res.status, data: null, error: errMsg };
    }
    return { ok: true, status: res.status, data: data as T };
  } catch (err: unknown) {
    // Sanitize the error message before surfacing it to API responses.
    // Node's fetch errors typically embed the full request URL, which on
    // jambonz includes the accountSid (e.g. /Accounts/<sid>/...). Strip
    // any http(s) URL substring so we never leak account identifiers via
    // upstream ValidationError messages.
    const raw = err instanceof Error ? err.message : 'Network error';
    const safe = raw.replace(/https?:\/\/\S+/g, '<jambonz-endpoint>');
    return { ok: false, status: 0, data: null, error: safe };
  }
}

export async function createCarrier(creds: JambonzCreds, input: JambonzCarrierInput): Promise<{ sid: string } | null> {
  const body = {
    name: input.name,
    requires_register: !!(input.username && input.password),
    register_username: input.username,
    register_password: input.password,
    register_sip_realm: input.sip_realm,
    register_from_user: input.username,
    register_from_domain: input.sip_realm,
    is_active: input.is_active !== false,
    smpp_system_id: null,
    e164_leading_plus: false,
    application_sid: null,
    sip_gateways: [
      {
        ipv4: input.outbound_proxy || input.sip_realm,
        port: input.port ?? defaultPortFor(input.transport),
        protocol: jambonzProtocol(input.transport),
        netmask: 32,
        inbound: true,
        outbound: true,
        is_active: true,
      },
    ],
  };
  const res = await jambonzFetch<{ sid: string }>(creds, `/Accounts/${creds.accountSid}/VoipCarriers`, {
    method: 'POST',
    body: JSON.stringify(body),
  });
  return res.ok && res.data ? { sid: res.data.sid } : null;
}

export async function updateCarrier(creds: JambonzCreds, sid: string, input: Partial<JambonzCarrierInput>): Promise<boolean> {
  const body: Record<string, unknown> = {};
  if (input.name !== undefined) body.name = input.name;
  if (input.is_active !== undefined) body.is_active = input.is_active;
  if (input.username !== undefined) body.register_username = input.username;
  if (input.password !== undefined) body.register_password = input.password;
  if (input.sip_realm !== undefined) body.register_sip_realm = input.sip_realm;
  // Refresh the gateway only when one of its fields changed; this lets the
  // caller PATCH credentials without resetting host/port/transport.
  if (input.sip_realm !== undefined || input.outbound_proxy !== undefined ||
      input.port !== undefined || input.transport !== undefined) {
    body.sip_gateways = [
      {
        ipv4: input.outbound_proxy || input.sip_realm,
        port: input.port ?? defaultPortFor(input.transport),
        protocol: jambonzProtocol(input.transport),
        netmask: 32,
        inbound: true,
        outbound: true,
        is_active: true,
      },
    ];
  }
  const res = await jambonzFetch(creds, `/VoipCarriers/${sid}`, { method: 'PUT', body: JSON.stringify(body) });
  return res.ok;
}

export async function deleteCarrier(creds: JambonzCreds, sid: string): Promise<boolean> {
  const res = await jambonzFetch(creds, `/VoipCarriers/${sid}`, { method: 'DELETE' });
  return res.ok;
}

/**
 * Probe a SIP carrier without persisting it. Performs a real REGISTER probe
 * against the jambonz `/SipGateways/test` endpoint and returns a structured
 * result with the round-trip latency and the raw upstream response so the
 * UI can show actionable diagnostics.
 *
 * Never returns ok:true on a 404 — a missing endpoint is an environment
 * mis-configuration, not a passing test.
 */
export async function testCarrier(
  creds: JambonzCreds,
  input: { sip_server: string; sip_username: string; sip_password: string; outbound_proxy?: string; sip_port?: number; sip_transport?: SipTransport }
): Promise<{ ok: boolean; latencyMs: number; response?: unknown; error?: string }> {
  const probeBody = {
    sip_realm: input.sip_server,
    username: input.sip_username,
    password: input.sip_password,
    outbound_proxy: input.outbound_proxy || input.sip_server,
    port: input.sip_port ?? defaultPortFor(input.sip_transport),
    protocol: jambonzProtocol(input.sip_transport),
  };
  const startedAt = Date.now();
  const res = await jambonzFetch<{ status?: string; reason?: string }>(
    creds,
    `/Accounts/${creds.accountSid}/SipGateways/test`,
    { method: 'POST', body: JSON.stringify(probeBody) }
  );
  const latencyMs = Date.now() - startedAt;

  if (res.ok && res.data?.status === 'ok') {
    return { ok: true, latencyMs, response: res.data };
  }
  const reason = res.data?.reason
    || res.error
    || (res.status === 404 ? 'jambonz test endpoint unavailable on this deployment' : `HTTP ${res.status}`);
  return { ok: false, latencyMs, response: res.data, error: reason };
}

export async function ensureApplication(
  creds: JambonzCreds,
  input: { name: string; call_hook: string; call_status_hook: string }
): Promise<{ sid: string } | null> {
  const body = {
    name: input.name,
    call_hook: { url: input.call_hook, method: 'POST' },
    call_status_hook: { url: input.call_status_hook, method: 'POST' },
    speech_synthesis_vendor: 'google',
    speech_recognizer_vendor: 'google',
  };
  const res = await jambonzFetch<{ sid: string }>(creds, `/Accounts/${creds.accountSid}/Applications`, {
    method: 'POST',
    body: JSON.stringify(body),
  });
  return res.ok && res.data ? { sid: res.data.sid } : null;
}

export async function updateApplication(
  creds: JambonzCreds,
  sid: string,
  input: { name?: string; call_hook?: string; call_status_hook?: string }
): Promise<boolean> {
  const body: Record<string, unknown> = {};
  if (input.name !== undefined) body.name = input.name;
  if (input.call_hook !== undefined) body.call_hook = { url: input.call_hook, method: 'POST' };
  if (input.call_status_hook !== undefined) body.call_status_hook = { url: input.call_status_hook, method: 'POST' };
  const res = await jambonzFetch(creds, `/Applications/${sid}`, { method: 'PUT', body: JSON.stringify(body) });
  return res.ok;
}

export async function deleteApplication(creds: JambonzCreds, sid: string): Promise<boolean> {
  const res = await jambonzFetch(creds, `/Applications/${sid}`, { method: 'DELETE' });
  return res.ok;
}

/**
 * Bind/unbind a carrier to an application. Required so inbound calls on the
 * carrier route to the application's call_hook (our /api/webhooks/sip/voice).
 */
export async function linkCarrierToApplication(
  creds: JambonzCreds,
  carrierSid: string,
  applicationSid: string | null
): Promise<boolean> {
  const res = await jambonzFetch(creds, `/VoipCarriers/${carrierSid}`, {
    method: 'PUT',
    body: JSON.stringify({ application_sid: applicationSid }),
  });
  return res.ok;
}

/**
 * Fetch the carrier record from jambonz so we can read live registration
 * state (used by the refresh endpoint to hydrate `sip_config.registration_status`).
 */
export async function getCarrier(
  creds: JambonzCreds,
  sid: string
): Promise<{ registered?: boolean; status?: string; raw?: unknown } | null> {
  const res = await jambonzFetch<Record<string, unknown>>(creds, `/VoipCarriers/${sid}`, { method: 'GET' });
  if (!res.ok || !res.data) return null;
  const reg = res.data as Record<string, unknown>;
  // jambonz exposes registration via gateway state; surface a simple flag.
  const gateways = (reg.sip_gateways as Array<Record<string, unknown>> | undefined) || [];
  const anyRegistered = gateways.some(g => g.registration_status === 'registered' || g.is_registered === true);
  return {
    registered: anyRegistered,
    status: anyRegistered ? 'registered' : 'unregistered',
    raw: res.data,
  };
}

export async function createCall(creds: JambonzCreds, input: JambonzCallInput): Promise<{ sid: string } | null> {
  const body = {
    from: input.from,
    to: input.to,
    application_sid: input.application_sid,
    call_hook: { url: input.call_hook, method: 'POST' },
    call_status_hook: input.call_status_hook ? { url: input.call_status_hook, method: 'POST' } : undefined,
  };
  const res = await jambonzFetch<{ sid: string }>(creds, `/Accounts/${creds.accountSid}/Calls`, {
    method: 'POST',
    body: JSON.stringify(body),
  });
  return res.ok && res.data ? { sid: res.data.sid } : null;
}
