import { createHmac } from 'node:crypto';
import {
  AccessToken,
  RoomServiceClient,
  TrackSource,
  type ParticipantInfo,
  type Room,
} from 'livekit-server-sdk';
import { config } from './config';

const PARTICIPANT_TOKEN_TTL = '10m';
const ROOM_DEPARTURE_TIMEOUT_SECONDS = 30 * 60;
const ROOM_MAX_CONNECTIONS = 5;

export interface LiveKitAdmission {
  provider: 'livekit';
  serverUrl: string;
  participantToken: string;
  roomName: string;
  encryptionKey: string;
  observer: boolean;
}

interface AdmissionInput {
  meetingId: string;
  sessionId: string;
  userId: string;
  displayName: string;
  role: string;
}

interface ObserverAdmissionInput {
  meetingId: string;
  adminUserId: string;
  displayName: string;
}

const roomNameFor = (meetingId: string): string => `family-${meetingId}`;
const supportRoomNameFor = (requestId: string): string => `support-${requestId}`;

const serviceUrl = (url: string): string => url
  .replace(/^wss:/i, 'https:')
  .replace(/^ws:/i, 'http:');

const credentials = () => {
  const { url, apiKey, apiSecret } = config.liveKit;
  if (!url || !apiKey || !apiSecret) throw new Error('LiveKit Cloud is not configured');
  return { url, apiKey, apiSecret };
};

const roomService = (): RoomServiceClient => {
  const { url, apiKey, apiSecret } = credentials();
  return new RoomServiceClient(serviceUrl(url), apiKey, apiSecret);
};

const encryptionKeyFor = (meetingId: string): string => createHmac(
  'sha256',
  config.contactDataSecret,
).update(`family-call-e2ee:${meetingId}`).digest('base64url');

const supportEncryptionKeyFor = (requestId: string): string => createHmac(
  'sha256',
  config.contactDataSecret,
).update(`support-call-e2ee:${requestId}`).digest('base64url');

export const liveKitReady = (): boolean => config.liveKit.configured;

export const ensureLiveKitRoom = async (meetingId: string): Promise<string> => {
  const roomName = roomNameFor(meetingId);
  const service = roomService();
  const existing = await service.listRooms([roomName]);
  if (existing.length > 0) return roomName;
  try {
    await service.createRoom({
      name: roomName,
      emptyTimeout: ROOM_DEPARTURE_TIMEOUT_SECONDS,
      departureTimeout: ROOM_DEPARTURE_TIMEOUT_SECONDS,
      maxParticipants: ROOM_MAX_CONNECTIONS,
      metadata: JSON.stringify({ meetingId, purpose: 'family-call' }),
    });
  } catch (error) {
    const raced = await service.listRooms([roomName]);
    if (raced.length === 0) throw error;
  }
  return roomName;
};

export const createLiveKitAdmission = async (
  input: AdmissionInput,
): Promise<LiveKitAdmission> => {
  const { url, apiKey, apiSecret } = credentials();
  const roomName = await ensureLiveKitRoom(input.meetingId);
  const token = new AccessToken(apiKey, apiSecret, {
    identity: `${input.userId}:${input.sessionId}`,
    name: input.displayName,
    metadata: JSON.stringify({
      sessionId: input.sessionId,
      userId: input.userId,
      role: input.role,
      observer: false,
    }),
    ttl: PARTICIPANT_TOKEN_TTL,
  });
  token.addGrant({
    room: roomName,
    roomJoin: true,
    canPublish: true,
    canPublishSources: [TrackSource.CAMERA, TrackSource.MICROPHONE],
    canPublishData: false,
    canSubscribe: true,
    canUpdateOwnMetadata: false,
  });

  return {
    provider: 'livekit',
    serverUrl: url,
    participantToken: await token.toJwt(),
    roomName,
    encryptionKey: encryptionKeyFor(input.meetingId),
    observer: false,
  };
};

export const createAdminObserverAdmission = async (
  input: ObserverAdmissionInput,
): Promise<LiveKitAdmission> => {
  const { url, apiKey, apiSecret } = credentials();
  const roomName = await ensureLiveKitRoom(input.meetingId);
  const token = new AccessToken(apiKey, apiSecret, {
    identity: `admin:${input.adminUserId}`,
    name: input.displayName,
    metadata: JSON.stringify({
      userId: input.adminUserId,
      role: 'adminObserver',
      observer: true,
    }),
    ttl: PARTICIPANT_TOKEN_TTL,
  });
  token.addGrant({
    room: roomName,
    roomJoin: true,
    canPublish: false,
    canPublishData: false,
    canSubscribe: true,
    canUpdateOwnMetadata: false,
    hidden: true,
  });
  return {
    provider: 'livekit',
    serverUrl: url,
    participantToken: await token.toJwt(),
    roomName,
    encryptionKey: encryptionKeyFor(input.meetingId),
    observer: true,
  };
};

export const createSupportCallAdmission = async (input: {
  requestId: string;
  userId: string;
  displayName: string;
  role: 'user' | 'admin';
}): Promise<LiveKitAdmission> => {
  const { url, apiKey, apiSecret } = credentials();
  const roomName = supportRoomNameFor(input.requestId);
  const service = roomService();
  if ((await service.listRooms([roomName])).length === 0) {
    try {
      await service.createRoom({
        name: roomName,
        emptyTimeout: 30 * 60,
        departureTimeout: 30 * 60,
        maxParticipants: 2,
        metadata: JSON.stringify({ supportRequestId: input.requestId, purpose: 'support-call' }),
      });
    } catch (error) {
      if ((await service.listRooms([roomName])).length === 0) throw error;
    }
  }
  const token = new AccessToken(apiKey, apiSecret, {
    identity: `support:${input.role}:${input.userId}`,
    name: input.displayName,
    metadata: JSON.stringify({ userId: input.userId, role: input.role, support: true }),
    ttl: PARTICIPANT_TOKEN_TTL,
  });
  token.addGrant({
    room: roomName,
    roomJoin: true,
    canPublish: true,
    canPublishSources: [TrackSource.MICROPHONE],
    canPublishData: false,
    canSubscribe: true,
    canUpdateOwnMetadata: false,
  });
  return {
    provider: 'livekit',
    serverUrl: url,
    participantToken: await token.toJwt(),
    roomName,
    encryptionKey: supportEncryptionKeyFor(input.requestId),
    observer: false,
  };
};

export const listLiveKitRooms = async (): Promise<Room[]> => roomService().listRooms();

export const listLiveKitParticipants = async (
  roomName: string,
): Promise<ParticipantInfo[]> => roomService().listParticipants(roomName);

export const removeLiveKitParticipant = async (
  roomName: string,
  identity: string,
): Promise<void> => roomService().removeParticipant(roomName, identity, {
  revokeTokenTs: BigInt(Math.floor(Date.now() / 1000)),
});

export const endLiveKitRoom = async (meetingId: string): Promise<void> => {
  await roomService().deleteRoom(roomNameFor(meetingId));
};
