import type { Client, CommandInteraction, GuildMember, Message } from "discord.js";
import { Connectors, LoadType, Shoukaku, type Player, type Track } from "shoukaku";
import { env } from "../../core/env.js";
import { logger } from "../../core/logger.js";

type QueueItem = {
  track: Track;
  requestedBy: string;
};

type GuildState = {
  queue: QueueItem[];
  textChannelId: string | null;
  listenersBound: boolean;
};

class MusicPlayerService {
  private shoukaku: Shoukaku | null = null;
  private readonly guildState = new Map<string, GuildState>();

  init(client: Client): void {
    if (this.shoukaku) return;

    this.shoukaku = new Shoukaku(
      new Connectors.DiscordJS(client),
      [
        {
          name: env.LAVALINK_NODE_NAME,
          url: `${env.LAVALINK_HOST}:${env.LAVALINK_PORT}`,
          auth: env.LAVALINK_PASSWORD,
          secure: env.LAVALINK_SECURE,
        },
      ],
      {
        resume: true,
        resumeTimeout: 60,
        reconnectTries: 999,
        reconnectInterval: 5,
        moveOnDisconnect: true,
      },
    );

    this.shoukaku.on("ready", (name, reconnected) => {
      logger.info({ name, reconnected }, "Lavalink node ready");
    });
    this.shoukaku.on("error", (name, error) => {
      logger.error({ name, error }, "Lavalink node error");
    });
    this.shoukaku.on("disconnect", (name, count) => {
      logger.warn({ name, count }, "Lavalink node disconnected");
    });
  }

  private getState(guildId: string): GuildState {
    const existing = this.guildState.get(guildId);
    if (existing) return existing;
    const created: GuildState = { queue: [], textChannelId: null, listenersBound: false };
    this.guildState.set(guildId, created);
    return created;
  }

  private getNode() {
    const node = this.shoukaku?.getIdealNode();
    if (!node) throw new Error("Lavalink node is not ready. Check lavalink server status.");
    return node;
  }

  private bindPlayerEvents(player: Player, guildId: string): void {
    const state = this.getState(guildId);
    if (state.listenersBound) return;

    player.on("end", async (event) => {
      if (["finished", "loadFailed", "stopped"].includes(event.reason)) {
        await this.playNext(guildId).catch((error) =>
          logger.error({ guildId, error }, "Failed to play next track after end"),
        );
      }
    });

    player.on("exception", (event) => {
      logger.error({ guildId, event }, "Track exception");
    });

    state.listenersBound = true;
  }

  private async ensurePlayer(member: GuildMember): Promise<Player> {
    if (!this.shoukaku) throw new Error("Music service is not initialized.");
    if (!member.voice.channelId) {
      throw new Error("먼저 음성 채널에 입장해주세요.");
    }

    const guildId = member.guild.id;
    // Always re-issue voice join to refresh session payload after reconnects.
    const player = await this.shoukaku.joinVoiceChannel({
      guildId,
      channelId: member.voice.channelId,
      shardId: member.guild.shardId ?? 0,
      deaf: true,
      mute: false,
    });

    await player.setPaused(false);
    await player.setGlobalVolume(100);
    this.bindPlayerEvents(player, guildId);
    return player;
  }

  private async searchTrack(query: string): Promise<Track[]> {
    const node = this.getNode();
    const identifier = /^https?:\/\//i.test(query) ? query : `ytsearch:${query}`;
    const result = await node.rest.resolve(identifier);
    if (!result) return [];

    if (result.loadType === LoadType.TRACK) return [result.data];
    if (result.loadType === LoadType.SEARCH) return result.data;
    if (result.loadType === LoadType.PLAYLIST) return result.data.tracks;
    return [];
  }

  private async playNext(guildId: string): Promise<void> {
    if (!this.shoukaku) return;
    const player = this.shoukaku.players.get(guildId);
    if (!player) return;

    const state = this.getState(guildId);
    const next = state.queue.shift();
    if (!next) return;

    await player.playTrack({ track: { encoded: next.track.encoded } });
  }

  private async enqueueCore(input: {
    guildId: string;
    channelId: string;
    userId: string;
    member: GuildMember;
    query: string;
  }): Promise<string> {
    const player = await this.ensurePlayer(input.member);
    const tracks = await this.searchTrack(input.query);
    if (tracks.length === 0) {
      throw new Error("재생 가능한 트랙을 찾지 못했습니다.");
    }

    const state = this.getState(input.guildId);
    state.textChannelId = input.channelId;

    const queueItems: QueueItem[] = tracks.map((track) => ({ track, requestedBy: input.userId }));

    if (!player.track) {
      const [first, ...rest] = queueItems;
      await player.playTrack({ track: { encoded: first.track.encoded } });
      state.queue.push(...rest);

      if (tracks.length > 1) {
        return `재생 시작: **${first.track.info.title}** (추가 ${tracks.length - 1}곡)`;
      }
      return `재생 시작: **${first.track.info.title}**`;
    }

    state.queue.push(...queueItems);
    return `큐에 추가됨: **${tracks[0].info.title}** (대기열 ${state.queue.length}개)`;
  }

  async enqueueFromInteraction(interaction: CommandInteraction, query: string): Promise<string> {
    if (!interaction.guildId || !interaction.guild || !interaction.channelId) {
      throw new Error("길드 채널에서만 사용할 수 있습니다.");
    }

    const member = interaction.member as GuildMember;
    return this.enqueueCore({
      guildId: interaction.guildId,
      channelId: interaction.channelId,
      userId: interaction.user.id,
      member,
      query,
    });
  }

  async enqueueFromMessage(message: Message, query: string): Promise<string> {
    if (!message.guildId || !message.guild || !message.channelId) {
      throw new Error("길드 채널에서만 사용할 수 있습니다.");
    }
    const member = message.member ?? (await message.guild.members.fetch(message.author.id));
    return this.enqueueCore({
      guildId: message.guildId,
      channelId: message.channelId,
      userId: message.author.id,
      member,
      query,
    });
  }

  async skip(guildId: string): Promise<string> {
    if (!this.shoukaku) throw new Error("Music service is not initialized.");
    const player = this.shoukaku.players.get(guildId);
    if (!player || !player.track) throw new Error("스킵할 트랙이 없습니다.");

    await player.stopTrack();
    return "현재 트랙을 스킵했습니다.";
  }

  async stop(guildId: string): Promise<string> {
    if (!this.shoukaku) throw new Error("Music service is not initialized.");
    const player = this.shoukaku.players.get(guildId);
    if (!player) throw new Error("활성 플레이어가 없습니다.");

    const state = this.getState(guildId);
    state.queue = [];
    await player.stopTrack();
    await this.shoukaku.leaveVoiceChannel(guildId);
    return "재생을 중지하고 음성 채널에서 나갔습니다.";
  }

  list(guildId: string): string[] {
    const state = this.getState(guildId);
    return state.queue.map((q, i) => `${i + 1}. ${q.track.info.title}`);
  }
}

export const musicPlayer = new MusicPlayerService();
