import {
  ServerEvents,
  StreamKind,
  type TChannelState,
  type TExternalStreamsMap,
  type TRemoteProducerIds,
  type TTransportParams,
  type TVoiceMap,
  type TVoiceUserState
} from '@pulse/shared';
import type {
  AppData,
  Consumer,
  Producer,
  Router,
  RouterOptions,
  WebRtcTransport,
  WebRtcTransportOptions
} from 'mediasoup/types';
import { eq } from 'drizzle-orm';
import { config, SERVER_PUBLIC_IP } from '../config';
import { db } from '../db';
import { getServerMemberIds } from '../db/queries/servers';
import { channels } from '../db/schema';
import { logger } from '../logger';
import { eventBus } from '../plugins/event-bus';
import { IS_PRODUCTION } from '../utils/env';
import { mediaSoupWorker } from '../utils/mediasoup';
import { pubsub } from '../utils/pubsub';

const voiceRuntimes = new Map<number, VoiceRuntime>();

const defaultRouterOptions: RouterOptions<AppData> = {
  mediaCodecs: [
    {
      kind: 'video',
      mimeType: 'video/H264',
      clockRate: 90000,
      parameters: {
        'packetization-mode': 1,
        'profile-level-id': '42e01f',
        'level-asymmetry-allowed': 1,
        'x-google-start-bitrate': 1000
      }
    },
    {
      kind: 'video',
      mimeType: 'video/VP8',
      clockRate: 90000,
      parameters: {
        'x-google-start-bitrate': 1000
      }
    },
    {
      kind: 'audio',
      mimeType: 'audio/opus',
      clockRate: 48000,
      channels: 2,
      parameters: {
        minptime: 10,
        useinbandfec: config.mediasoup.audio.fec ? 1 : 0,
        usedtx: config.mediasoup.audio.dtx ? 1 : 0,
        stereo: config.mediasoup.audio.stereo ? 1 : 0,
        maxplaybackrate: 48000,
        maxaveragebitrate: +config.mediasoup.audio.maxBitrate,
        'sprop-stereo': config.mediasoup.audio.stereo ? 1 : 0
      }
    }
  ]
};

const getRtcTransportOptions = (): WebRtcTransportOptions<AppData> => {
  const ip = IS_PRODUCTION ? '0.0.0.0' : '127.0.0.1';
  const announcedAddress = IS_PRODUCTION ? SERVER_PUBLIC_IP : undefined;

  const listenInfo = announcedAddress
    ? { ip, announcedAddress }
    : { ip };

  logger.debug(
    `WebRTC transport options: ip=${ip}, announcedAddress=${announcedAddress ?? 'none'}, IS_PRODUCTION=${IS_PRODUCTION}`
  );

  return {
    listenInfos: [
      { protocol: 'udp' as const, ...listenInfo },
      { protocol: 'tcp' as const, ...listenInfo }
    ],
    enableUdp: true,
    enableTcp: true,
    preferUdp: true,
    preferTcp: false,
    initialAvailableOutgoingBitrate: config.mediasoup.video.initialAvailableOutgoingBitrate
  };
};

const defaultUserState: TVoiceUserState = {
  micMuted: false,
  soundMuted: false,
  webcamEnabled: false,
  sharingScreen: false
};

type TTransportMap = {
  [userId: number]: WebRtcTransport<AppData>;
};

type TProducerMap = {
  [userId: number]: Producer<AppData>;
};

type TConsumerMap = {
  [userId: number]: {
    [remoteId: number]: Consumer<AppData>;
  };
};

type TExternalStreamProducers = {
  audioProducer?: Producer<AppData>;
  videoProducer?: Producer<AppData>;
};

type TExternalStreamInternal = {
  title: string;
  key: string;
  pluginId: string;
  avatarUrl?: string;
  producers: TExternalStreamProducers;
};

class VoiceRuntime {
  public readonly id: number;
  public readonly isDmVoice: boolean;
  private state: TChannelState = { users: [], externalStreams: {} };
  private router?: Router<AppData>;
  private consumerTransports: TTransportMap = {};
  private producerTransports: TTransportMap = {};
  private videoProducers: TProducerMap = {};
  private audioProducers: TProducerMap = {};
  private screenProducers: TProducerMap = {};
  private screenAudioProducers: TProducerMap = {};
  private consumers: TConsumerMap = {};

  private _destroying = false;
  private externalCounter = 0;
  private externalStreamsInternal: {
    [streamId: number]: TExternalStreamInternal;
  } = {};
  private _cachedServerId: number | undefined;

  constructor(channelId: number, isDmVoice = false) {
    this.id = channelId;
    this.isDmVoice = isDmVoice;
    voiceRuntimes.set(channelId, this);
  }

  public static findById = (channelId: number): VoiceRuntime | undefined => {
    return voiceRuntimes.get(channelId);
  };

  public static findRuntimeByUserId = (
    userId: number
  ): VoiceRuntime | undefined => {
    for (const runtime of voiceRuntimes.values()) {
      if (runtime.getUser(userId)) {
        return runtime;
      }
    }

    return undefined;
  };

  public static getVoiceMap = (channelIds?: Set<number>): TVoiceMap => {
    const map: TVoiceMap = {};

    voiceRuntimes.forEach((runtime, channelId) => {
      if (channelIds && !channelIds.has(channelId)) return;

      const channelState = runtime.getState();

      // Skip empty runtimes (no active users)
      if (channelState.users.length === 0) return;

      const entry: TVoiceMap[number] = {
        users: {},
        startedAt: channelState.startedAt
      };

      channelState.users.forEach((user) => {
        entry.users[user.userId] = user.state;
      });

      map[channelId] = entry;
    });

    return map;
  };

  public static getExternalStreamsMap = (
    channelIds?: Set<number>
  ): TExternalStreamsMap => {
    const map: TExternalStreamsMap = {};

    voiceRuntimes.forEach((runtime, channelId) => {
      if (channelIds && !channelIds.has(channelId)) return;

      map[channelId] = runtime.getState().externalStreams;
    });

    return map;
  };

  private getServerMemberIdsForChannel = async (): Promise<number[]> => {
    if (this.isDmVoice) return [];
    if (!this._cachedServerId) {
      const [ch] = await db
        .select({ serverId: channels.serverId })
        .from(channels)
        .where(eq(channels.id, this.id))
        .limit(1);
      if (ch) this._cachedServerId = ch.serverId;
    }
    if (!this._cachedServerId) return [];
    return getServerMemberIds(this._cachedServerId);
  };

  public init = async (): Promise<void> => {
    logger.debug(`Initializing voice runtime for channel ${this.id}`);

    await this.createRouter();

    eventBus.emit('voice:runtime_initialized', {
      channelId: this.id
    });
  };

  public destroy = async () => {
    if (this._destroying) return;
    this._destroying = true;

    await this.router?.close();

    Object.values(this.consumerTransports).forEach((transport) => {
      transport.close();
    });

    Object.values(this.producerTransports).forEach((transport) => {
      transport.close();
    });

    Object.values(this.videoProducers).forEach((producer) => {
      producer.close();
    });

    Object.values(this.screenProducers).forEach((producer) => {
      producer.close();
    });

    Object.values(this.screenAudioProducers).forEach((producer) => {
      producer.close();
    });

    Object.values(this.audioProducers).forEach((producer) => {
      producer.close();
    });

    Object.values(this.externalStreamsInternal).forEach((stream) => {
      if (
        stream.producers.videoProducer &&
        !stream.producers.videoProducer.closed
      ) {
        stream.producers.videoProducer.close();
      }
      if (
        stream.producers.audioProducer &&
        !stream.producers.audioProducer.closed
      ) {
        stream.producers.audioProducer.close();
      }
    });

    Object.values(this.consumers).forEach((consumers) => {
      Object.values(consumers).forEach((consumer) => {
        consumer.close();
      });
    });

    voiceRuntimes.delete(this.id);

    eventBus.emit('voice:runtime_closed', {
      channelId: this.id
    });
  };

  public getState = (): TChannelState => {
    return this.state;
  };

  public getUser = (userId: number) => {
    return this.state.users.find((u) => u.userId === userId);
  };

  public getUserState = (userId: number): TVoiceUserState => {
    const user = this.getUser(userId);

    return user?.state ?? defaultUserState;
  };

  public addUser = (
    userId: number,
    state: Pick<TVoiceUserState, 'micMuted' | 'soundMuted'>
  ) => {
    if (this.getUser(userId)) return;

    if (this.state.users.length === 0) {
      this.state.startedAt = Date.now();
    }

    this.state.users.push({
      userId,
      state: {
        ...defaultUserState,
        ...state
      }
    });
  };

  public removeUser = (userId: number) => {
    this.state.users = this.state.users.filter((u) => u.userId !== userId);

    if (this.state.users.length === 0) {
      this.state.startedAt = undefined;
    }

    this.cleanupUserResources(userId);
  };

  private cleanupUserResources = (userId: number) => {
    this.removeProducerTransport(userId);
    this.removeConsumerTransport(userId);

    this.removeProducer(userId, StreamKind.AUDIO);
    this.removeProducer(userId, StreamKind.VIDEO);
    this.removeProducer(userId, StreamKind.SCREEN);
    this.removeProducer(userId, StreamKind.SCREEN_AUDIO);

    if (this.consumers[userId]) {
      Object.values(this.consumers[userId]).forEach((consumer) => {
        consumer.close();
      });

      delete this.consumers[userId];
    }

    Object.keys(this.consumers).forEach((consumerUserIdStr) => {
      const consumerId = parseInt(consumerUserIdStr);

      if (consumerId !== userId && this.consumers[consumerId]?.[userId]) {
        this.consumers[consumerId][userId].close();

        delete this.consumers[consumerId][userId];
      }
    });
  };

  public updateUserState = (
    userId: number,
    newState: Partial<TChannelState['users'][0]['state']>
  ) => {
    const user = this.getUser(userId);

    if (!user) return;

    user.state = { ...user.state, ...newState };
  };

  public getRouter = (): Router<AppData> => {
    if (!this.router) {
      throw new Error('Router not initialized yet');
    }

    return this.router;
  };

  private createRouter = async () => {
    const router = await mediaSoupWorker.createRouter(defaultRouterOptions);

    this.router = router;
  };

  public createTransport = async () => {
    const router = this.getRouter();

    const transport = await router.createWebRtcTransport(
      getRtcTransportOptions()
    );

    logger.debug(
      `Transport created: id=${transport.id}, iceCandidates=${JSON.stringify(transport.iceCandidates)}`
    );

    const params: TTransportParams = {
      id: transport.id,
      iceParameters: transport.iceParameters,
      iceCandidates: transport.iceCandidates,
      dtlsParameters: transport.dtlsParameters
    };

    return { transport, params };
  };

  public createConsumerTransport = async (userId: number) => {
    // Close any existing transport to prevent leaks from duplicate requests
    const existing = this.consumerTransports[userId];
    if (existing) {
      existing.close();
    }

    const { transport, params } = await this.createTransport();

    this.consumerTransports[userId] = transport;

    transport.observer.on('close', () => {
      delete this.consumerTransports[userId];

      if (this.consumers[userId]) {
        Object.values(this.consumers[userId]).forEach((consumer) => {
          consumer.close();
        });

        delete this.consumers[userId];
      }
    });

    transport.on('dtlsstatechange', (state) => {
      if (state === 'failed' || state === 'closed') {
        this.removeConsumerTransport(userId);
      }
    });

    return params;
  };

  public removeConsumerTransport = (userId: number) => {
    const transport = this.consumerTransports[userId];

    if (!transport) return;

    transport.close();
  };

  public getConsumerTransport = (userId: number) => {
    return this.consumerTransports[userId];
  };

  public createProducerTransport = async (userId: number) => {
    // Close any existing transport to prevent leaks from duplicate requests
    const existing = this.producerTransports[userId];
    if (existing) {
      existing.close();
    }

    const { params, transport } = await this.createTransport();

    this.producerTransports[userId] = transport;

    transport.observer.on('close', () => {
      delete this.producerTransports[userId];

      this.removeProducer(userId, StreamKind.AUDIO);
      this.removeProducer(userId, StreamKind.VIDEO);
      this.removeProducer(userId, StreamKind.SCREEN);
      this.removeProducer(userId, StreamKind.SCREEN_AUDIO);
    });

    transport.on('dtlsstatechange', (state) => {
      if (state === 'failed' || state === 'closed') {
        this.removeProducerTransport(userId);
      }
    });

    return params;
  };

  public removeProducerTransport = (userId: number) => {
    const transport = this.producerTransports[userId];

    if (!transport) return;

    transport.close();
  };

  public getProducerTransport = (userId: number) => {
    return this.producerTransports[userId];
  };

  public getProducer = (type: StreamKind, id: number) => {
    switch (type) {
      case StreamKind.VIDEO:
        return this.videoProducers[id];
      case StreamKind.AUDIO:
        return this.audioProducers[id];
      case StreamKind.SCREEN:
        return this.screenProducers[id];
      case StreamKind.SCREEN_AUDIO:
        return this.screenAudioProducers[id];
      case StreamKind.EXTERNAL_VIDEO:
        return this.externalStreamsInternal[id]?.producers.videoProducer;
      case StreamKind.EXTERNAL_AUDIO:
        return this.externalStreamsInternal[id]?.producers.audioProducer;
      default:
        return undefined;
    }
  };

  public addProducer = (
    userId: number,
    type: StreamKind,
    producer: Producer
  ) => {
    if (type === StreamKind.VIDEO) {
      this.videoProducers[userId] = producer;
    } else if (type === StreamKind.AUDIO) {
      this.audioProducers[userId] = producer;
    } else if (type === StreamKind.SCREEN) {
      this.screenProducers[userId] = producer;
    } else if (type === StreamKind.SCREEN_AUDIO) {
      this.screenAudioProducers[userId] = producer;
    }

    producer.observer.on('close', () => {
      if (type === StreamKind.VIDEO) {
        delete this.videoProducers[userId];
      } else if (type === StreamKind.AUDIO) {
        delete this.audioProducers[userId];
      } else if (type === StreamKind.SCREEN) {
        delete this.screenProducers[userId];
      } else if (type === StreamKind.SCREEN_AUDIO) {
        delete this.screenAudioProducers[userId];
      }

      // Notify peers that this producer is gone so they can clean up consumers
      this.getServerMemberIdsForChannel()
        .then((memberIds) => {
          pubsub.publishFor(memberIds, ServerEvents.VOICE_PRODUCER_CLOSED, {
            channelId: this.id,
            remoteId: userId,
            kind: type
          });
        })
        .catch((err) => {
          logger.error('[VoiceRuntime] Failed to broadcast producer close for user %d:', userId, err);
        });
    });
  };

  public removeProducer(userId: number, type: StreamKind) {
    let producer: Producer | undefined;

    switch (type) {
      case StreamKind.VIDEO:
        producer = this.videoProducers[userId];
        break;
      case StreamKind.AUDIO:
        producer = this.audioProducers[userId];
        break;
      case StreamKind.SCREEN:
        producer = this.screenProducers[userId];
        break;
      case StreamKind.SCREEN_AUDIO:
        producer = this.screenAudioProducers[userId];
        break;
      default:
        return;
    }

    if (!producer) return;

    producer.close();

    if (type === StreamKind.VIDEO) {
      delete this.videoProducers[userId];
    } else if (type === StreamKind.AUDIO) {
      delete this.audioProducers[userId];
    } else if (type === StreamKind.SCREEN) {
      delete this.screenProducers[userId];
    } else if (type === StreamKind.SCREEN_AUDIO) {
      delete this.screenAudioProducers[userId];
    }
  }

  public addConsumer = (
    userId: number,
    remoteId: number,
    consumer: Consumer<AppData>
  ) => {
    if (!this.consumers[userId]) {
      this.consumers[userId] = {};
    }

    this.consumers[userId][remoteId] = consumer;

    consumer.observer.on('close', () => {
      delete this.consumers[userId]?.[remoteId];
    });
  };

  public createExternalStream = (options: {
    title: string;
    key: string;
    pluginId: string;
    avatarUrl?: string;
    producers: {
      audio?: Producer;
      video?: Producer;
    };
  }) => {
    const streamId = this.externalCounter++;

    const { title, key, pluginId, avatarUrl, producers } = options;

    this.externalStreamsInternal[streamId] = {
      title,
      key,
      pluginId,
      avatarUrl,
      producers: {
        audioProducer: producers.audio,
        videoProducer: producers.video
      }
    };

    if (producers.audio) {
      this.setupExternalProducerCloseHandler(
        streamId,
        'audio',
        producers.audio
      );
    }

    if (producers.video) {
      this.setupExternalProducerCloseHandler(
        streamId,
        'video',
        producers.video
      );
    }

    this.state.externalStreams[streamId] = {
      title,
      key,
      pluginId,
      avatarUrl,
      tracks: {
        audio: !!producers.audio,
        video: !!producers.video
      }
    };

    return streamId;
  };

  private setupExternalProducerCloseHandler = (
    streamId: number,
    kind: 'audio' | 'video',
    producer: Producer
  ) => {
    producer.observer.on('close', () => {
      const internal = this.externalStreamsInternal[streamId];

      if (!internal) return;

      if (kind === 'audio') {
        delete internal.producers.audioProducer;
      } else {
        delete internal.producers.videoProducer;
      }

      const hasProducers =
        internal.producers.audioProducer || internal.producers.videoProducer;

      if (!hasProducers) {
        this.removeExternalStream(streamId);
      } else {
        const existingStream = this.state.externalStreams[streamId];

        if (existingStream) {
          existingStream.tracks = {
            audio: !!internal.producers.audioProducer,
            video: !!internal.producers.videoProducer
          };

          this.getServerMemberIdsForChannel().then((memberIds) => {
            pubsub.publishFor(
              memberIds,
              ServerEvents.VOICE_UPDATE_EXTERNAL_STREAM,
              {
                channelId: this.id,
                streamId,
                stream: existingStream
              }
            );
          }).catch(() => {});
        }
      }
    });
  };

  public removeExternalStream = (streamId: number) => {
    const internal = this.externalStreamsInternal[streamId];

    if (!internal) return;

    if (
      internal.producers.audioProducer &&
      !internal.producers.audioProducer.closed
    ) {
      internal.producers.audioProducer.close();
    }
    if (
      internal.producers.videoProducer &&
      !internal.producers.videoProducer.closed
    ) {
      internal.producers.videoProducer.close();
    }

    delete this.externalStreamsInternal[streamId];
    delete this.state.externalStreams[streamId];

    this.getServerMemberIdsForChannel().then((memberIds) => {
      pubsub.publishFor(memberIds, ServerEvents.VOICE_REMOVE_EXTERNAL_STREAM, {
        channelId: this.id,
        streamId
      });
    }).catch(() => {});
  };

  public updateExternalStream = (
    streamId: number,
    options: {
      title?: string;
      avatarUrl?: string;
      producers?: {
        audio?: Producer;
        video?: Producer;
      };
    }
  ) => {
    const internal = this.externalStreamsInternal[streamId];

    if (!internal) return;

    const publicStream = this.state.externalStreams[streamId];

    if (!publicStream) return;

    if (options.title !== undefined) {
      internal.title = options.title;
      publicStream.title = options.title;
    }

    if (options.avatarUrl !== undefined) {
      internal.avatarUrl = options.avatarUrl;
      publicStream.avatarUrl = options.avatarUrl;
    }

    if (options.producers) {
      if (options.producers.audio !== undefined) {
        if (
          internal.producers.audioProducer &&
          !internal.producers.audioProducer.closed
        ) {
          internal.producers.audioProducer.close();
        }

        if (options.producers.audio) {
          internal.producers.audioProducer = options.producers.audio;
          this.setupExternalProducerCloseHandler(
            streamId,
            'audio',
            options.producers.audio
          );

          pubsub.publishForChannel(this.id, ServerEvents.VOICE_NEW_PRODUCER, {
            channelId: this.id,
            remoteId: streamId,
            kind: StreamKind.EXTERNAL_AUDIO
          });
        } else {
          delete internal.producers.audioProducer;
        }
      }

      if (options.producers.video !== undefined) {
        if (
          internal.producers.videoProducer &&
          !internal.producers.videoProducer.closed
        ) {
          internal.producers.videoProducer.close();
        }

        if (options.producers.video) {
          internal.producers.videoProducer = options.producers.video;
          this.setupExternalProducerCloseHandler(
            streamId,
            'video',
            options.producers.video
          );

          pubsub.publishForChannel(this.id, ServerEvents.VOICE_NEW_PRODUCER, {
            channelId: this.id,
            remoteId: streamId,
            kind: StreamKind.EXTERNAL_VIDEO
          });
        } else {
          delete internal.producers.videoProducer;
        }
      }

      publicStream.tracks = {
        audio: !!internal.producers.audioProducer,
        video: !!internal.producers.videoProducer
      };
    }

    this.getServerMemberIdsForChannel().then((memberIds) => {
      pubsub.publishFor(memberIds, ServerEvents.VOICE_UPDATE_EXTERNAL_STREAM, {
        channelId: this.id,
        streamId,
        stream: publicStream
      });
    }).catch(() => {});
  };

  public getExternalStreamProducer = (
    streamId: number,
    kind: 'audio' | 'video'
  ): Producer | undefined => {
    const internal = this.externalStreamsInternal[streamId];
    if (!internal) return undefined;

    return kind === 'audio'
      ? internal.producers.audioProducer
      : internal.producers.videoProducer;
  };

  public getRemoteIds = (userId: number): TRemoteProducerIds => {
    return {
      remoteVideoIds: Object.keys(this.videoProducers)
        .filter((id) => +id !== userId)
        .map((id) => +id),
      remoteAudioIds: Object.keys(this.audioProducers)
        .filter((id) => +id !== userId)
        .map((id) => +id),
      remoteScreenIds: Object.keys(this.screenProducers)
        .filter((id) => +id !== userId)
        .map((id) => +id),
      remoteScreenAudioIds: Object.keys(this.screenAudioProducers)
        .filter((id) => +id !== userId)
        .map((id) => +id),
      remoteExternalStreamIds: Object.keys(this.externalStreamsInternal).map(
        (id) => +id
      )
    };
  };

  public getExternalStreamTracks = (
    streamId: number
  ): { audio: boolean; video: boolean } => {
    const internal = this.externalStreamsInternal[streamId];
    if (!internal) return { audio: false, video: false };

    return {
      audio: !!internal.producers.audioProducer,
      video: !!internal.producers.videoProducer
    };
  };

  public static getListenInfo = () => {
    const ip = IS_PRODUCTION ? '0.0.0.0' : '127.0.0.1';
    const announcedAddress = IS_PRODUCTION ? SERVER_PUBLIC_IP : undefined;

    return { ip, announcedAddress };
  };
}

export { VoiceRuntime };
