import {
  ActionRowBuilder,
  ButtonBuilder,
  ButtonInteraction,
  ButtonStyle,
  EmbedBuilder,
  GuildMember,
} from 'discord.js';
import {
  AudioPlayer,
  AudioPlayerStatus,
  NoSubscriberBehavior,
  StreamType,
  VoiceConnection,
  VoiceConnectionStatus,
  createAudioPlayer,
  createAudioResource,
  entersState,
  joinVoiceChannel,
} from '@discordjs/voice';
import ffmpegPath from 'ffmpeg-static';
import { ChildProcess, execFile, spawn } from 'child_process';
import { promisify } from 'util';
import { SupportedLocale, t } from '../i18n';
import { logger } from '../utils/logger';

const execFileAsync = promisify(execFile);

type MusicTextChannel = {
  id: string;
  send: (options: Record<string, unknown>) => Promise<any>;
};

interface MusicQueueItem {
  videoId: string;
  title: string;
  durationSec: number | null;
  durationText: string;
  sourceUrl: string;
  requestedByUserId: string;
  requestedByName: string;
}

interface PlaylistExtractionResult {
  title: string | null;
  tracks: Array<{
    videoId: string;
    title: string;
    durationSec: number | null;
    durationText: string | null;
  }>;
}

interface GuildMusicSession {
  guildId: string;
  voiceChannelId: string;
  textChannel: MusicTextChannel;
  locale: SupportedLocale;
  connection: VoiceConnection;
  player: AudioPlayer;
  queue: MusicQueueItem[];
  nowPlaying: MusicQueueItem | null;
  nowPlayingStartedAt: number | null;
  pausedStartedAt: number | null;
  accumulatedPausedMs: number;
  ffmpegProcess: ChildProcess | null;
  controlMessage: any | null;
  progressInterval: NodeJS.Timeout | null;
}

type SearchVideoResult = {
  type?: string;
  id?: string;
  video_id?: string;
  title?: { text?: string; toString?: () => string } | string;
  duration?: { text?: string } | string;
};

export function extractYouTubeVideoId(raw: string): string | null {
  const trimmed = raw.trim();
  if (/^[\w-]{11}$/.test(trimmed)) {
    return trimmed;
  }

  try {
    const url = new URL(trimmed);
    const host = url.hostname.replace(/^www\./, '');

    if (host === 'youtu.be') {
      const id = url.pathname.split('/').filter(Boolean)[0];
      return id && /^[\w-]{11}$/.test(id) ? id : null;
    }

    if (host === 'youtube.com' || host === 'm.youtube.com' || host === 'music.youtube.com') {
      if (url.pathname === '/watch') {
        const id = url.searchParams.get('v');
        return id && /^[\w-]{11}$/.test(id) ? id : null;
      }

      const segments = url.pathname.split('/').filter(Boolean);
      const candidate = segments[1];
      if (
        segments.length >= 2 &&
        ['embed', 'shorts', 'live'].includes(segments[0]) &&
        candidate &&
        /^[\w-]{11}$/.test(candidate)
      ) {
        return candidate;
      }
    }
  } catch {
    return null;
  }

  return null;
}

export function isYouTubePlaylistUrl(raw: string): boolean {
  try {
    const url = new URL(raw.trim());
    const host = url.hostname.replace(/^www\./, '');

    if (!['youtube.com', 'm.youtube.com', 'music.youtube.com'].includes(host)) {
      return false;
    }

    return Boolean(url.searchParams.get('list'));
  } catch {
    return false;
  }
}

function parseDurationSeconds(value: unknown): number | null {
  if (typeof value === 'number' && Number.isFinite(value)) {
    return value;
  }

  if (typeof value === 'string' && /^\d+$/.test(value)) {
    return Number(value);
  }

  return null;
}

function parseDurationTextToSeconds(value?: string | null): number | null {
  if (!value) {
    return null;
  }

  const parts = value
    .split(':')
    .map((part) => part.trim())
    .filter(Boolean);

  if (parts.length === 0 || parts.some((part) => !/^\d+$/.test(part))) {
    return null;
  }

  const numbers = parts.map(Number);
  if (numbers.length === 3) {
    return (numbers[0] * 3600) + (numbers[1] * 60) + numbers[2];
  }

  if (numbers.length === 2) {
    return (numbers[0] * 60) + numbers[1];
  }

  if (numbers.length === 1) {
    return numbers[0];
  }

  return null;
}

export function formatDuration(seconds: number | null, fallback?: string | null): string {
  if (seconds === null || !Number.isFinite(seconds)) {
    return fallback || 'Unknown';
  }

  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const secs = seconds % 60;

  if (hours > 0) {
    return [hours, minutes, secs].map((value, index) => (index === 0 ? String(value) : String(value).padStart(2, '0'))).join(':');
  }

  return [minutes, secs].map((value) => String(value).padStart(2, '0')).join(':');
}

export class MusicService {
  private static sessions = new Map<string, GuildMusicSession>();
  private static youtubePromise: Promise<any> | null = null;
  private static readonly FFMPEG_USER_AGENT =
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36';
  private static readonly PYTHON_EXECUTABLE =
    'C:\\Users\\NSG-C0522\\AppData\\Local\\Microsoft\\WindowsApps\\python.exe';

  private static async getYouTube() {
    if (!this.youtubePromise) {
      this.youtubePromise = import('youtubei.js').then(({ Innertube }) => Innertube.create());
    }

    return this.youtubePromise;
  }

  static async addFromQuery(member: GuildMember, textChannel: MusicTextChannel, query: string, locale: SupportedLocale) {
    const youtube = await this.getYouTube();
    const search = await youtube.search(query, { type: 'video' });
    const video = search.results.find((item: SearchVideoResult) => item.type === 'Video' && (item.id || item.video_id)) as SearchVideoResult | undefined;

    if (!video) {
      throw new Error('NO_SEARCH_RESULTS');
    }

    const videoId = video.id || video.video_id;
    if (!videoId) {
      throw new Error('NO_SEARCH_RESULTS');
    }

    const title =
      typeof video.title === 'string'
        ? video.title
        : video.title?.text || video.title?.toString?.() || videoId;
    const durationText = typeof video.duration === 'string' ? video.duration : video.duration?.text;

    const track = await this.createQueueItem(member, videoId, title, durationText);
    return this.enqueueTracks(member, textChannel, [track], locale);
  }

  static async addFromUrl(member: GuildMember, textChannel: MusicTextChannel, rawUrl: string, locale: SupportedLocale) {
    if (isYouTubePlaylistUrl(rawUrl)) {
      const playlist = await this.extractPlaylist(rawUrl);
      if (playlist.tracks.length === 0) {
        throw new Error('NO_SEARCH_RESULTS');
      }

      const tracks = playlist.tracks.map((track) => ({
        videoId: track.videoId,
        title: track.title,
        durationSec: track.durationSec,
        durationText: formatDuration(track.durationSec, track.durationText),
        sourceUrl: `https://www.youtube.com/watch?v=${track.videoId}`,
        requestedByUserId: member.id,
        requestedByName: member.displayName,
      }));

      return this.enqueueTracks(member, textChannel, tracks, locale);
    }

    const videoId = extractYouTubeVideoId(rawUrl);
    if (!videoId) {
      throw new Error('INVALID_URL');
    }

    const track = await this.createQueueItem(member, videoId);
    return this.enqueueTracks(member, textChannel, [track], locale);
  }

  static getQueueEmbed(guildId: string, locale: SupportedLocale) {
    const session = this.sessions.get(guildId);
    const embed = new EmbedBuilder()
      .setColor(0xff5555)
      .setTitle(t(locale, 'commands.music.queueTitle'));

    if (!session || (!session.nowPlaying && session.queue.length === 0)) {
      embed.setDescription(t(locale, 'commands.music.queueEmpty'));
      return embed;
    }

    if (session.nowPlaying) {
      embed.addFields({
        name: t(locale, 'commands.music.queueNowPlaying'),
        value: this.formatTrackLine(session.nowPlaying),
      });
    }

    if (session.queue.length > 0) {
      const visibleTracks = session.queue.slice(0, 10);
      embed.addFields({
        name: t(locale, 'commands.music.queueUpcoming'),
        value: visibleTracks
          .map((track, index) => `\`${index + 1}\` ${this.formatTrackLine(track)}`)
          .join('\n'),
      });

      if (session.queue.length > visibleTracks.length) {
        embed.addFields({
          name: '\u200b',
          value: t(locale, 'commands.music.queueMoreItems', {
            count: String(session.queue.length - visibleTracks.length),
          }),
        });
      }
    }

    return embed;
  }

  static getActiveVoiceChannelId(guildId: string) {
    return this.sessions.get(guildId)?.voiceChannelId || null;
  }

  static async skip(guildId: string) {
    const session = this.sessions.get(guildId);
    if (!session || !session.nowPlaying) {
      return false;
    }

    session.player.stop(true);
    return true;
  }

  static async pause(guildId: string, locale: SupportedLocale) {
    const session = this.sessions.get(guildId);
    if (!session || !session.nowPlaying) {
      return false;
    }

    if (this.isPaused(session)) {
      return true;
    }

    session.locale = locale;
    session.player.pause(true);
    if (!session.pausedStartedAt) {
      session.pausedStartedAt = Date.now();
    }

    await this.refreshControlMessage(session);
    return true;
  }

  static async resume(guildId: string, locale: SupportedLocale) {
    const session = this.sessions.get(guildId);
    if (!session || !session.nowPlaying) {
      return false;
    }

    session.locale = locale;
    if (session.pausedStartedAt) {
      session.accumulatedPausedMs += Date.now() - session.pausedStartedAt;
      session.pausedStartedAt = null;
    }

    session.player.unpause();
    await this.refreshControlMessage(session);
    return true;
  }

  static async stop(guildId: string, locale: SupportedLocale) {
    const session = this.sessions.get(guildId);
    if (!session) {
      return false;
    }

    session.queue = [];
    session.locale = locale;
    session.player.stop(true);
    session.nowPlaying = null;
    session.nowPlayingStartedAt = null;
    session.pausedStartedAt = null;
    session.accumulatedPausedMs = 0;
    await this.renderIdleState(session);
    return true;
  }

  static async remove(guildId: string, index: number) {
    const session = this.sessions.get(guildId);
    if (!session || session.queue.length === 0) {
      return null;
    }

    const queueIndex = index - 1;
    if (queueIndex < 0 || queueIndex >= session.queue.length) {
      throw new Error('QUEUE_INDEX_OUT_OF_RANGE');
    }

    const removed = session.queue.splice(queueIndex, 1)[0] ?? null;
    await this.refreshControlMessage(session);
    return removed;
  }

  static async leave(guildId: string, locale: SupportedLocale) {
    const session = this.sessions.get(guildId);
    if (!session) {
      return false;
    }

    session.queue = [];
    session.locale = locale;
    this.cleanupProcess(session);
    session.player.stop(true);
    session.nowPlaying = null;
    session.nowPlayingStartedAt = null;
    session.pausedStartedAt = null;
    session.accumulatedPausedMs = 0;
    session.connection.destroy();
    await this.renderIdleState(session);
    this.sessions.delete(guildId);
    return true;
  }

  static async handleControlInteraction(interaction: ButtonInteraction, locale: SupportedLocale) {
    const session = interaction.guildId ? this.sessions.get(interaction.guildId) : null;
    if (!interaction.guildId || !session) {
      await interaction.update({
        components: [this.buildFallbackControlRow(interaction.guildId ?? 'stale')],
      });
      await interaction.followUp({
        content: t(locale, 'commands.music.noActiveSession'),
        ephemeral: true,
      });
      return;
    }

    const member = interaction.member as GuildMember;
    if (!member.voice.channelId || member.voice.channelId !== session.voiceChannelId) {
      await interaction.reply({
        content: t(locale, 'commands.music.differentVoiceChannel'),
        ephemeral: true,
      });
      return;
    }

    const action = interaction.customId.split('_')[1];
    await interaction.deferUpdate();

    if (action === 'pause') {
      await this.pause(interaction.guildId, locale);
      return;
    }

    if (action === 'resume') {
      await this.resume(interaction.guildId, locale);
      return;
    }

    if (action === 'skip') {
      await this.skip(interaction.guildId);
      return;
    }

    if (action === 'stop') {
      await this.stop(interaction.guildId, locale);
      return;
    }

    if (action === 'leave') {
      await this.leave(interaction.guildId, locale);
    }
  }

  private static async createQueueItem(
    member: GuildMember,
    videoId: string,
    fallbackTitle?: string,
    fallbackDurationText?: string,
  ): Promise<MusicQueueItem> {
    const youtube = await this.getYouTube();
    const info = await youtube.getBasicInfo(videoId);
    const basicInfo = info.basic_info as Record<string, unknown> | undefined;
    const durationSec = parseDurationSeconds(basicInfo?.duration_seconds) ?? parseDurationTextToSeconds(fallbackDurationText);

    return {
      videoId,
      title: typeof basicInfo?.title === 'string' ? basicInfo.title : fallbackTitle || videoId,
      durationSec,
      durationText: formatDuration(durationSec, fallbackDurationText),
      sourceUrl: `https://www.youtube.com/watch?v=${videoId}`,
      requestedByUserId: member.id,
      requestedByName: member.displayName,
    };
  }

  private static async enqueueTracks(
    member: GuildMember,
    textChannel: MusicTextChannel,
    tracks: MusicQueueItem[],
    locale: SupportedLocale,
  ) {
    const session = await this.ensureSession(member, textChannel, locale);
    const shouldStartNow = !session.nowPlaying;

    session.locale = locale;
    session.textChannel = textChannel;
    const previousLength = session.queue.length;
    session.queue.push(...tracks);

    if (shouldStartNow) {
      void this.playNext(session.guildId);
    } else {
      await this.refreshControlMessage(session);
    }

    return {
      track: tracks[0],
      tracksAdded: tracks.length,
      startedNow: shouldStartNow,
      voiceChannelId: session.voiceChannelId,
      position: previousLength + 1,
    };
  }

  private static async ensureSession(member: GuildMember, textChannel: MusicTextChannel, locale: SupportedLocale) {
    const voiceChannel = member.voice.channel;
    if (!voiceChannel) {
      throw new Error('NOT_IN_VOICE');
    }

    const existing = this.sessions.get(member.guild.id);
    if (existing) {
      if (existing.voiceChannelId !== voiceChannel.id) {
        throw new Error('DIFFERENT_VOICE');
      }

      existing.textChannel = textChannel;
      existing.locale = locale;
      return existing;
    }

    const connection = joinVoiceChannel({
      channelId: voiceChannel.id,
      guildId: member.guild.id,
      adapterCreator: member.guild.voiceAdapterCreator as any,
      selfDeaf: false,
    });

    await entersState(connection, VoiceConnectionStatus.Ready, 20_000);

    const player = createAudioPlayer({
      behaviors: {
        noSubscriber: NoSubscriberBehavior.Pause,
      },
    });

    connection.subscribe(player);

    const session: GuildMusicSession = {
      guildId: member.guild.id,
      voiceChannelId: voiceChannel.id,
      textChannel,
      locale,
      connection,
      player,
      queue: [],
      nowPlaying: null,
      nowPlayingStartedAt: null,
      pausedStartedAt: null,
      accumulatedPausedMs: 0,
      ffmpegProcess: null,
      controlMessage: null,
      progressInterval: null,
    };

    this.bindSessionEvents(session);
    this.sessions.set(member.guild.id, session);
    return session;
  }

  private static bindSessionEvents(session: GuildMusicSession) {
    session.player.on(AudioPlayerStatus.Idle, () => {
      void this.onTrackFinished(session.guildId);
    });

    session.player.on('error', (error) => {
      logger.error(`Music player error in guild ${session.guildId}:`, error);
      void this.notifyPlaybackFailure(session);
    });

    session.connection.on(VoiceConnectionStatus.Disconnected, () => {
      this.cleanupProcess(session);
      this.sessions.delete(session.guildId);
    });
  }

  private static async onTrackFinished(guildId: string) {
    const session = this.sessions.get(guildId);
    if (!session) {
      return;
    }

    this.cleanupProcess(session);
    session.nowPlaying = null;
    session.nowPlayingStartedAt = null;
    session.pausedStartedAt = null;
    session.accumulatedPausedMs = 0;

    if (session.queue.length === 0) {
      await this.renderIdleState(session);
      return;
    }

    await this.playNext(guildId);
  }

  private static async notifyPlaybackFailure(session: GuildMusicSession) {
    const failedTrack = session.nowPlaying;
    this.cleanupProcess(session);
    session.nowPlaying = null;
    session.nowPlayingStartedAt = null;
    session.pausedStartedAt = null;
    session.accumulatedPausedMs = 0;

    if (failedTrack) {
      await this.safeSend(session.textChannel, {
        content: `${t(session.locale, 'commands.music.playbackFailed', { title: failedTrack.title })}\n${t(session.locale, 'commands.music.playbackFailedResolution')}`,
      });
    }

    if (session.queue.length > 0) {
      await this.playNext(session.guildId);
    } else {
      await this.renderIdleState(session);
    }
  }

  private static async playNext(guildId: string) {
    const session = this.sessions.get(guildId);
    if (!session) {
      return;
    }

    const nextTrack = session.queue.shift();
    if (!nextTrack) {
      session.nowPlaying = null;
      session.nowPlayingStartedAt = null;
      await this.renderIdleState(session);
      return;
    }

    session.nowPlaying = nextTrack;
    session.nowPlayingStartedAt = Date.now();
    session.pausedStartedAt = null;
    session.accumulatedPausedMs = 0;

    try {
      const streamUrl = await this.resolveStreamUrl(nextTrack.videoId);
      const process = this.spawnFfmpeg(streamUrl);
      session.ffmpegProcess = process;

      const resource = createAudioResource(process.stdout, {
        inputType: StreamType.Raw,
        metadata: nextTrack,
      });

      session.player.play(resource);
      await this.renderNowPlaying(session);
    } catch (error) {
      logger.error(`Failed to start playback for ${nextTrack.videoId}:`, error);
      await this.safeSend(session.textChannel, {
        content: `${t(session.locale, 'commands.music.streamUnavailable')}\n${t(session.locale, 'commands.music.streamUnavailableResolution')}`,
      });
      session.nowPlaying = null;
      session.nowPlayingStartedAt = null;
      await this.playNext(guildId);
    }
  }

  private static async resolveStreamUrl(videoId: string): Promise<string> {
    const videoUrl = `https://www.youtube.com/watch?v=${videoId}`;

    try {
      const { stdout } = await execFileAsync(
        this.PYTHON_EXECUTABLE,
        [
          '-m',
          'yt_dlp',
          '--no-playlist',
          '--no-warnings',
          '--extractor-args',
          'youtube:player_client=android,web',
          '-f',
          'bestaudio/best',
          '-g',
          videoUrl,
        ],
        {
          windowsHide: true,
          timeout: 30_000,
          maxBuffer: 1024 * 1024,
        },
      );

      const resolvedUrl = stdout
        .split(/\r?\n/)
        .map((line) => line.trim())
        .find((line) => line.startsWith('http'));

      if (resolvedUrl) {
        return resolvedUrl;
      }
    } catch (error) {
      logger.warn(`yt-dlp stream extraction failed for ${videoId}:`, error);
    }

    const youtube = await this.getYouTube();
    const info = await youtube.getInfo(videoId);
    const streamingData = info.streaming_data as Record<string, unknown> | undefined;
    const serverAbrUrl = typeof streamingData?.server_abr_streaming_url === 'string' ? streamingData.server_abr_streaming_url : null;
    const hlsManifestUrl = typeof streamingData?.hls_manifest_url === 'string' ? streamingData.hls_manifest_url : null;
    const streamUrl = hlsManifestUrl || serverAbrUrl;

    if (!streamUrl) {
      throw new Error('STREAM_UNAVAILABLE');
    }

    return streamUrl;
  }

  private static async extractPlaylist(rawUrl: string): Promise<PlaylistExtractionResult> {
    const { stdout } = await execFileAsync(
      this.PYTHON_EXECUTABLE,
      [
        '-m',
        'yt_dlp',
        '--flat-playlist',
        '--dump-single-json',
        '--no-warnings',
        '--playlist-end',
        '100',
        rawUrl,
      ],
      {
        windowsHide: true,
        timeout: 30_000,
        maxBuffer: 8 * 1024 * 1024,
      },
    );

    const parsed = JSON.parse(stdout) as {
      title?: string;
      entries?: Array<{
        id?: string;
        title?: string;
        duration?: number | null;
        duration_string?: string | null;
      }>;
    };

    return {
      title: parsed.title ?? null,
      tracks: (parsed.entries ?? [])
        .filter((entry) => entry.id && entry.title)
        .map((entry) => ({
          videoId: entry.id!,
          title: entry.title!,
          durationSec: typeof entry.duration === 'number' ? entry.duration : parseDurationTextToSeconds(entry.duration_string ?? null),
          durationText: entry.duration_string ?? null,
        })),
    };
  }

  private static spawnFfmpeg(streamUrl: string) {
    if (!ffmpegPath) {
      throw new Error('FFMPEG_NOT_FOUND');
    }

    const process = spawn(ffmpegPath, [
      '-user_agent',
      this.FFMPEG_USER_AGENT,
      '-headers',
      'Referer: https://www.youtube.com\r\nOrigin: https://www.youtube.com\r\n',
      '-reconnect',
      '1',
      '-reconnect_streamed',
      '1',
      '-reconnect_delay_max',
      '5',
      '-i',
      streamUrl,
      '-vn',
      '-loglevel',
      'error',
      '-f',
      's16le',
      '-ar',
      '48000',
      '-ac',
      '2',
      'pipe:1',
    ], {
      stdio: ['ignore', 'pipe', 'pipe'],
    });

    process.stderr.on('data', (chunk) => {
      const message = chunk.toString().trim();
      if (message) {
        logger.debug(`[Music FFmpeg] ${message}`);
      }
    });

    return process;
  }

  private static cleanupProcess(session: GuildMusicSession) {
    this.stopProgressUpdates(session);

    if (session.ffmpegProcess && !session.ffmpegProcess.killed) {
      session.ffmpegProcess.kill();
    }
    session.ffmpegProcess = null;
  }

  private static async renderNowPlaying(session: GuildMusicSession) {
    if (!session.nowPlaying) {
      return;
    }

    const embed = this.buildNowPlayingEmbed(session);
    const components = [this.buildControlRow(session, false)];

    if (session.controlMessage) {
      await session.controlMessage.edit({ embeds: [embed], components });
    } else {
      session.controlMessage = await this.safeSend(session.textChannel, { embeds: [embed], components });
    }

    this.startProgressUpdates(session);
  }

  private static async renderIdleState(session: GuildMusicSession) {
    this.stopProgressUpdates(session);

    if (!session.controlMessage) {
      return;
    }

    const embed = new EmbedBuilder()
      .setColor(0x5865f2)
      .setTitle(t(session.locale, 'commands.music.playbackIdleTitle'))
      .setDescription(t(session.locale, 'commands.music.playbackIdleBody'));

    await session.controlMessage.edit({
      embeds: [embed],
      components: [this.buildControlRow(session, true)],
    });
  }

  private static buildControlRow(session: GuildMusicSession, disabled: boolean) {
    const paused = this.isPaused(session);
    return new ActionRowBuilder<ButtonBuilder>().addComponents(
      new ButtonBuilder()
        .setCustomId(`music_${paused ? 'resume' : 'pause'}_${session.guildId}`)
        .setEmoji(paused ? '▶️' : '⏸️')
        .setStyle(ButtonStyle.Success)
        .setDisabled(disabled),
      new ButtonBuilder()
        .setCustomId(`music_skip_${session.guildId}`)
        .setEmoji('⏭️')
        .setStyle(ButtonStyle.Secondary)
        .setDisabled(disabled),
      new ButtonBuilder()
        .setCustomId(`music_stop_${session.guildId}`)
        .setEmoji('⏹️')
        .setStyle(ButtonStyle.Danger)
        .setDisabled(disabled),
      new ButtonBuilder()
        .setCustomId(`music_leave_${session.guildId}`)
        .setEmoji('👋')
        .setStyle(ButtonStyle.Primary)
        .setDisabled(false),
    );
  }

  private static buildFallbackControlRow(guildId: string) {
    return new ActionRowBuilder<ButtonBuilder>().addComponents(
      new ButtonBuilder()
        .setCustomId(`music_pause_${guildId}`)
        .setEmoji('⏸️')
        .setStyle(ButtonStyle.Success)
        .setDisabled(true),
      new ButtonBuilder()
        .setCustomId(`music_skip_${guildId}`)
        .setEmoji('⏭️')
        .setStyle(ButtonStyle.Secondary)
        .setDisabled(true),
      new ButtonBuilder()
        .setCustomId(`music_stop_${guildId}`)
        .setEmoji('⏹️')
        .setStyle(ButtonStyle.Danger)
        .setDisabled(true),
      new ButtonBuilder()
        .setCustomId(`music_leave_${guildId}`)
        .setEmoji('👋')
        .setStyle(ButtonStyle.Primary)
        .setDisabled(true),
    );
  }

  private static buildNowPlayingEmbed(session: GuildMusicSession) {
    const track = session.nowPlaying!;
    const durationSec = track.durationSec;
    const elapsedSec = this.getElapsedSeconds(session);
    const boundedElapsed = durationSec !== null ? Math.min(elapsedSec, durationSec) : elapsedSec;
    const progressText =
      durationSec !== null
        ? `${formatDuration(boundedElapsed)} ${this.buildProgressBar(boundedElapsed, durationSec)} ${formatDuration(durationSec)}`
        : `${formatDuration(boundedElapsed)} ${this.buildIndeterminateProgressBar(boundedElapsed)} ${t(session.locale, 'commands.music.unknownDuration')}`;
    const nextTrack = session.queue[0] ?? null;
    const statusKey = this.isPaused(session) ? 'commands.music.statusPaused' : 'commands.music.statusPlaying';

    const embed = new EmbedBuilder()
      .setColor(0xff5555)
      .setTitle(t(session.locale, 'commands.music.playbackStartedTitle'))
      .setDescription(`**${track.title}**`)
      .addFields(
        {
          name: t(session.locale, 'commands.music.status'),
          value: t(session.locale, statusKey),
          inline: true,
        },
        {
          name: t(session.locale, 'commands.music.requestedBy'),
          value: `<@${track.requestedByUserId}>`,
          inline: true,
        },
        {
          name: t(session.locale, 'commands.music.duration'),
          value: track.durationText,
          inline: true,
        },
        {
          name: t(session.locale, 'commands.music.queueLength'),
          value: String(session.queue.length),
          inline: true,
        },
        {
          name: t(session.locale, 'commands.music.progress'),
          value: progressText,
        },
        {
          name: t(session.locale, 'commands.music.source'),
          value: track.sourceUrl,
        },
      );

    if (nextTrack) {
      embed.addFields({
        name: t(session.locale, 'commands.music.nextTrack'),
        value: this.formatTrackLine(nextTrack),
      });
    }

    return embed;
  }

  private static buildProgressBar(elapsedSec: number, durationSec: number) {
    const width = 12;
    const ratio = durationSec > 0 ? Math.min(1, elapsedSec / durationSec) : 0;
    const filled = Math.max(1, Math.min(width, Math.round(ratio * width)));
    return `${'■'.repeat(filled)}${'□'.repeat(width - filled)}`;
  }

  private static buildIndeterminateProgressBar(elapsedSec: number) {
    const width = 12;
    const position = Math.abs(Math.floor(elapsedSec / 5)) % width;
    return Array.from({ length: width }, (_, index) => (index === position ? '■' : '□')).join('');
  }

  private static startProgressUpdates(session: GuildMusicSession) {
    this.stopProgressUpdates(session);

    if (!session.controlMessage || !session.nowPlaying) {
      return;
    }

    session.progressInterval = setInterval(() => {
      if (!session.controlMessage || !session.nowPlaying) {
        this.stopProgressUpdates(session);
        return;
      }

      void session.controlMessage.edit({
        embeds: [this.buildNowPlayingEmbed(session)],
        components: [this.buildControlRow(session, false)],
      }).catch((error: unknown) => {
        logger.warn('Failed to update music progress message:', error);
        this.stopProgressUpdates(session);
      });
    }, 5_000);
  }

  private static stopProgressUpdates(session: GuildMusicSession) {
    if (session.progressInterval) {
      clearInterval(session.progressInterval);
      session.progressInterval = null;
    }
  }

  private static isPaused(session: GuildMusicSession) {
    return session.player.state.status === AudioPlayerStatus.Paused || session.pausedStartedAt !== null;
  }

  private static getElapsedSeconds(session: GuildMusicSession) {
    if (!session.nowPlayingStartedAt) {
      return 0;
    }

    const activeUntil = session.pausedStartedAt ?? Date.now();
    const elapsedMs = Math.max(0, activeUntil - session.nowPlayingStartedAt - session.accumulatedPausedMs);
    return Math.floor(elapsedMs / 1000);
  }

  private static async refreshControlMessage(session: GuildMusicSession) {
    if (!session.controlMessage) {
      return;
    }

    if (!session.nowPlaying) {
      await this.renderIdleState(session);
      return;
    }

    await session.controlMessage.edit({
      embeds: [this.buildNowPlayingEmbed(session)],
      components: [this.buildControlRow(session, false)],
    });

    if (this.isPaused(session)) {
      this.stopProgressUpdates(session);
    } else {
      this.startProgressUpdates(session);
    }
  }

  private static formatTrackLine(track: MusicQueueItem) {
    return `**${track.title}** \`${track.durationText}\` • <@${track.requestedByUserId}>`;
  }

  private static async safeSend(channel: MusicTextChannel, payload: Record<string, unknown>) {
    try {
      return await channel.send(payload);
    } catch (error) {
      logger.error('Failed to send music message:', error);
      return null;
    }
  }
}
