// SPDX-FileCopyrightText: 2025 LiveKit, Inc.
//
// SPDX-License-Identifier: Apache-2.0
import { Duration } from '@bufbuild/protobuf';
import type {
  ConnectTwilioCallRequest_TwilioCallDirection,
  DisconnectWhatsAppCallRequest_DisconnectReason,
  RoomAgentDispatch,
  SessionDescription,
} from '@livekit/protocol';
import {
  AcceptWhatsAppCallRequest,
  AcceptWhatsAppCallResponse,
  ConnectTwilioCallRequest,
  ConnectTwilioCallResponse,
  ConnectWhatsAppCallRequest,
  ConnectWhatsAppCallResponse,
  DialWhatsAppCallRequest,
  DialWhatsAppCallResponse,
  DisconnectWhatsAppCallRequest,
  DisconnectWhatsAppCallResponse,
} from '@livekit/protocol';
import type { ClientOptions } from './ClientOptions.js';
import { ServiceBase } from './ServiceBase.js';
import { type Rpc, TwirpRpc, livekitPackage } from './TwirpRPC.js';
import { DEFAULT_RINGING_TIMEOUT_SECONDS } from './dialTimeout.js';

const svc = 'Connector';

// WhatsApp types
export interface DialWhatsAppCallOptions {
  /** Required - The identifier of the WhatsApp phone number that is initiating the call */
  whatsappPhoneNumberId: string;
  /** Required - The number of the user that is supposed to receive the call */
  whatsappToPhoneNumber: string;
  /** Required - The API key of the business that is initiating the call */
  whatsappApiKey: string;
  /** Required - WhatsApp Cloud API version, eg: 23.0, 24.0, etc. */
  whatsappCloudApiVersion: string;
  /** Optional - An arbitrary string you can pass in that is useful for tracking and logging purposes */
  whatsappBizOpaqueCallbackData?: string;
  /** Optional - What LiveKit room should this participant be connected to */
  roomName?: string;
  /** Optional - Agents to dispatch the call to */
  agents?: RoomAgentDispatch[];
  /** Optional - Identity of the participant in LiveKit room */
  participantIdentity?: string;
  /** Optional - Name of the participant in LiveKit room */
  participantName?: string;
  /** Optional - User-defined metadata. Will be attached to a created Participant in the room. */
  participantMetadata?: string;
  /** Optional - User-defined attributes. Will be attached to a created Participant in the room. */
  participantAttributes?: { [key: string]: string };
  /** Optional - Country where the call terminates as ISO 3166-1 alpha-2 */
  destinationCountry?: string;
  /** Optional - Max time in seconds for the callee to answer the call */
  ringingTimeout?: number;
}

export interface AcceptWhatsAppCallOptions {
  /** Required - The identifier of the WhatsApp phone number that is connecting the call */
  whatsappPhoneNumberId: string;
  /** Required - The API key of the business that is connecting the call */
  whatsappApiKey: string;
  /** Required - WhatsApp Cloud API version, eg: 23.0, 24.0, etc. */
  whatsappCloudApiVersion: string;
  /** Required - Call ID sent by Meta */
  whatsappCallId: string;
  /** Optional - An arbitrary string you can pass in that is useful for tracking and logging purposes */
  whatsappBizOpaqueCallbackData?: string;
  /** Required - The call accept webhook comes with SDP from Meta */
  sdp: SessionDescription;
  /** Optional - What LiveKit room should this participant be connected to */
  roomName?: string;
  /** Optional - Agents to dispatch the call to */
  agents?: RoomAgentDispatch[];
  /** Optional - Identity of the participant in LiveKit room */
  participantIdentity?: string;
  /** Optional - Name of the participant in LiveKit room */
  participantName?: string;
  /** Optional - User-defined metadata. Will be attached to a created Participant in the room. */
  participantMetadata?: string;
  /** Optional - User-defined attributes. Will be attached to a created Participant in the room. */
  participantAttributes?: { [key: string]: string };
  /** Optional - Country where the call terminates as ISO 3166-1 alpha-2 */
  destinationCountry?: string;
  /** Optional - Wait until the inbound party joins before returning. */
  waitUntilAnswered?: boolean;
  /**
   * Optional - Request timeout in seconds. When `waitUntilAnswered` is set it
   * defaults to the standard ring window; otherwise the client default applies.
   */
  timeout?: number;
}

// Twilio types
export interface ConnectTwilioCallOptions {
  /** The direction of the call */
  twilioCallDirection: ConnectTwilioCallRequest_TwilioCallDirection;
  /** What LiveKit room should this call be connected to */
  roomName: string;
  /** Optional agents to dispatch the call to */
  agents?: RoomAgentDispatch[];
  /** Optional identity of the participant in LiveKit room */
  participantIdentity?: string;
  /** Optional name of the participant in LiveKit room */
  participantName?: string;
  /** Optional user-defined metadata. Will be attached to a created Participant in the room. */
  participantMetadata?: string;
  /** Optional user-defined attributes. Will be attached to a created Participant in the room. */
  participantAttributes?: { [key: string]: string };
  /** Country where the call terminates as ISO 3166-1 alpha-2 */
  destinationCountry?: string;
}

/**
 * Client to access Connector APIs for WhatsApp and Twilio integrations
 */
export class ConnectorClient extends ServiceBase {
  private readonly rpc: Rpc;

  /**
   * @param host - hostname including protocol. i.e. 'https://<project>.livekit.cloud'
   * @param apiKey - API Key, can be set in env var LIVEKIT_API_KEY
   * @param secret - API Secret, can be set in env var LIVEKIT_API_SECRET
   * @param options - client options
   */
  constructor(host: string, apiKey?: string, secret?: string, options?: ClientOptions) {
    super({ apiKey, secret, token: options?.token });
    this.rpc = new TwirpRpc(host, livekitPackage, {
      requestTimeout: options?.requestTimeout,
      failover: options?.failover,
    });
  }

  /**
   * Initiate an outbound WhatsApp call
   *
   * @param options - WhatsApp call options
   * @returns Promise containing the WhatsApp call ID and room name
   */
  async dialWhatsAppCall(options: DialWhatsAppCallOptions): Promise<DialWhatsAppCallResponse> {
    const whatsappBizOpaqueCallbackData = options.whatsappBizOpaqueCallbackData || '';
    const roomName = options.roomName || '';
    const participantIdentity = options.participantIdentity || '';
    const participantName = options.participantName || '';
    const participantMetadata = options.participantMetadata || '';
    const destinationCountry = options.destinationCountry || '';

    const req = new DialWhatsAppCallRequest({
      whatsappPhoneNumberId: options.whatsappPhoneNumberId,
      whatsappToPhoneNumber: options.whatsappToPhoneNumber,
      whatsappApiKey: options.whatsappApiKey,
      whatsappCloudApiVersion: options.whatsappCloudApiVersion,
      whatsappBizOpaqueCallbackData,
      roomName,
      agents: options.agents,
      participantIdentity,
      participantName,
      participantMetadata,
      participantAttributes: options.participantAttributes,
      destinationCountry,
      ringingTimeout: options.ringingTimeout
        ? new Duration({ seconds: BigInt(options.ringingTimeout) })
        : undefined,
    }).toJson();

    const data = await this.rpc.request(
      svc,
      'DialWhatsAppCall',
      req,
      await this.authHeader({ roomCreate: true }),
    );
    return DialWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true });
  }

  /**
   * Accept an inbound WhatsApp call
   *
   * @param options - WhatsApp call accept options
   * @returns Promise containing the room name
   */
  async acceptWhatsAppCall(
    options: AcceptWhatsAppCallOptions,
  ): Promise<AcceptWhatsAppCallResponse> {
    const whatsappBizOpaqueCallbackData = options.whatsappBizOpaqueCallbackData || '';
    const roomName = options.roomName || '';
    const participantIdentity = options.participantIdentity || '';
    const participantName = options.participantName || '';
    const participantMetadata = options.participantMetadata || '';
    const destinationCountry = options.destinationCountry || '';

    const req = new AcceptWhatsAppCallRequest({
      whatsappPhoneNumberId: options.whatsappPhoneNumberId,
      whatsappApiKey: options.whatsappApiKey,
      whatsappCloudApiVersion: options.whatsappCloudApiVersion,
      whatsappCallId: options.whatsappCallId,
      whatsappBizOpaqueCallbackData,
      sdp: options.sdp,
      roomName,
      agents: options.agents,
      participantIdentity,
      participantName,
      participantMetadata,
      participantAttributes: options.participantAttributes,
      destinationCountry,
      waitUntilAnswered: options.waitUntilAnswered,
    }).toJson();

    // When waiting for the inbound party to join, the request can block, so
    // default the timeout to the standard ring window; otherwise the client
    // default applies.
    const timeout = options.waitUntilAnswered
      ? (options.timeout ?? DEFAULT_RINGING_TIMEOUT_SECONDS)
      : options.timeout;

    const data = await this.rpc.request(
      svc,
      'AcceptWhatsAppCall',
      req,
      await this.authHeader({ roomCreate: true }),
      timeout,
    );
    return AcceptWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true });
  }

  /**
   * Connect an established WhatsApp call (used for business-initiated calls)
   *
   * @param whatsappCallId - Call ID sent by Meta
   * @param sdp - Session description from Meta
   */
  async connectWhatsAppCall(
    whatsappCallId: string,
    sdp: SessionDescription,
  ): Promise<ConnectWhatsAppCallResponse> {
    const req = new ConnectWhatsAppCallRequest({
      whatsappCallId,
      sdp,
    }).toJson();

    const data = await this.rpc.request(
      svc,
      'ConnectWhatsAppCall',
      req,
      await this.authHeader({ roomCreate: true }),
    );
    return ConnectWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true });
  }

  /**
   * Disconnect an active WhatsApp call
   *
   * @param whatsappCallId - Call ID sent by Meta
   * @param whatsappApiKey - The API key of the business that is disconnecting the call.
   *   Required when `disconnectReason` is BUSINESS_INITIATED, optional for USER_INITIATED.
   * @param disconnectReason - Optional reason for disconnecting the call. Defaults to BUSINESS_INITIATED.
   */
  async disconnectWhatsAppCall(
    whatsappCallId: string,
    whatsappApiKey: string,
    disconnectReason?: DisconnectWhatsAppCallRequest_DisconnectReason,
  ): Promise<DisconnectWhatsAppCallResponse> {
    const req = new DisconnectWhatsAppCallRequest({
      whatsappCallId,
      whatsappApiKey,
      disconnectReason,
    }).toJson();

    const data = await this.rpc.request(
      svc,
      'DisconnectWhatsAppCall',
      req,
      await this.authHeader({ roomCreate: true }),
    );
    return DisconnectWhatsAppCallResponse.fromJson(data, { ignoreUnknownFields: true });
  }

  /**
   * Connect a Twilio call to a LiveKit room
   *
   * @param options - Twilio call connection options
   * @returns Promise containing the WebSocket connect URL for Twilio media stream
   */
  async connectTwilioCall(options: ConnectTwilioCallOptions): Promise<ConnectTwilioCallResponse> {
    const participantIdentity = options.participantIdentity || '';
    const participantName = options.participantName || '';
    const participantMetadata = options.participantMetadata || '';
    const destinationCountry = options.destinationCountry || '';

    const req = new ConnectTwilioCallRequest({
      twilioCallDirection: options.twilioCallDirection,
      roomName: options.roomName,
      agents: options.agents,
      participantIdentity,
      participantName,
      participantMetadata,
      participantAttributes: options.participantAttributes,
      destinationCountry,
    }).toJson();

    const data = await this.rpc.request(
      svc,
      'ConnectTwilioCall',
      req,
      await this.authHeader({ roomCreate: true }),
    );
    return ConnectTwilioCallResponse.fromJson(data, { ignoreUnknownFields: true });
  }
}
