import {
  SlashCommandBuilder,
  SlashCommandStringOption, // Import for typing _options
  PermissionFlagsBits,
  ChannelType,
  EmbedBuilder,
  ChatInputCommandInteraction,
  GuildMember,
  VoiceBasedChannel,
} from "discord.js";
import logger from "../utils/logger";
import { BotClient } from "../index";
// Import necessary Shoukaku types - LavalinkResponse might need a local definition if not exported
import { Player, Node, Track, SearchResult, Connection } from "shoukaku";

// Define the structure of the Lavalink V4 response (if not directly available from shoukaku types)
// Based on https://lavalink.dev/api/rest.html#load-tracks
type LavalinkLoadType = "track" | "playlist" | "search" | "empty" | "error";

interface LavalinkResponse {
  loadType: LavalinkLoadType;
  data: any; // Data structure varies based on loadType
}

interface LavalinkErrorData {
  message: string;
  severity: string;
  cause: string;
}

interface LavalinkPlaylistInfo {
  name: string;
  selectedTrack?: number; // Optional index of the selected track within the playlist
}

interface LavalinkPlaylistData {
  info: LavalinkPlaylistInfo;
  pluginInfo: any; // Or specific type if known
  tracks: Track[];
}

// Export: Extend Player type locally to add queue and textChannelId
export interface GuildPlayer extends Player {
  queue: TrackWithRequester[];
  textChannelId?: string; // Optional: Store text channel ID for messages
}

// Export: Define TrackWithRequester
export interface TrackWithRequester extends Track {
  // Ensure encoded is strictly string if extending base Track which might have it optional
  encoded: string;
  requester: {
    id: string;
    tag: string;
  };
}

// Export: Helper function to start playback if possible
export async function playNext(player: GuildPlayer, _interaction: ChatInputCommandInteraction) {
  // Check if player is still valid (might have been destroyed)
  const shoukaku = (_interaction.client as BotClient).shoukaku;
  if (!shoukaku?.players.has(player.guildId)) {
    logger.warn(`playNext called for destroyed player in guild ${player.guildId}`);
    return;
  }

  if (player.track || player.queue.length === 0) {
    return; // Already playing or queue is empty
  }
  const nextTrack = player.queue.shift();
  if (!nextTrack) return;

  try {
    // Check if user provided an OAuth token (could be stored in a database or env variable)
    const oauthToken = process.env.YOUTUBE_OAUTH_TOKEN;
    const userData = oauthToken ? { "oauth-token": oauthToken } : undefined;
    // Fix: Correct usage for playTrack based on Player.ts
    await player.playTrack({ track: { encoded: nextTrack.encoded, userData: userData } });
    // logger.info(`Started playing: ${nextTrack.info.title} in guild ${player.guildId}`);
  } catch (playError: unknown) {
    const errorMsg = playError instanceof Error ? playError.message : String(playError);
    logger.error(
      `Error playing track ${nextTrack.info.title} in guild ${player.guildId}: ${errorMsg}`,
    );
    // Try to send error message to the stored text channel
    const channel = _interaction.guild?.channels.cache.get(
      player.textChannelId || _interaction.channelId,
    );
    if (channel?.isTextBased()) {
      // Fix: Check if e is Error before accessing message
      channel
        .send(`Error playing track: ${nextTrack.info.title}. Reason: ${errorMsg}`)
        .catch((e: unknown) => {
          const sendErrorMsg = e instanceof Error ? e.message : String(e);
          logger.error(`Failed to send play error message: ${sendErrorMsg}`);
        });
    }
    // Try playing the next track if available
    await playNext(player, _interaction);
  }
}

export default {
  data: new SlashCommandBuilder()
    .setName("play")
    .setDescription("Plays audio from a URL or search query")
    .addStringOption(
      (
        option: SlashCommandStringOption, // Type option
      ) =>
        option
          .setName("query")
          .setDescription("The URL or search term for the song/playlist")
          .setRequired(true),
    )
    .addStringOption(
      (
        option: SlashCommandStringOption, // Type option
      ) =>
        option
          .setName("source")
          .setDescription("Specify the search source (defaults to YouTube Music)")
          .setRequired(false)
          .addChoices(
            { name: "YouTube Music", value: "youtubemusic" },
            { name: "YouTube", value: "youtube" },
            { name: "SoundCloud", value: "soundcloud" },
            // Add other sources like 'spotify' if supported by Lavalink plugins
          ),
    ),
  async execute(_interaction: ChatInputCommandInteraction, _client: BotClient) {
    // Ensure command is run in a guild
    if (!_interaction.guildId || !_interaction.guild || !_interaction.channelId) {
      return _interaction
        .reply({ content: "This command can only be used in a server.", ephemeral: true })
        .catch(() => {});
    }
    if (!(_interaction.member instanceof GuildMember)) {
      return _interaction
        .reply({ content: "Could not determine your voice channel.", ephemeral: true })
        .catch(() => {});
    }

    await _interaction.deferReply(); // Defer reply immediately

    const member = _interaction.member;
    const voiceChannel = member?.voice?.channel;
    const query = _interaction.options.getString("query", true); // Required option
    const source = _interaction.options.getString("source"); // Optional

    // 1. Check if user is in a voice channel
    if (!voiceChannel) {
      return _interaction.editReply("You need to be in a voice channel to play music!");
    }
    const currentVoiceChannel = voiceChannel as VoiceBasedChannel;

    // 2. Check bot permissions
    const permissions = currentVoiceChannel.permissionsFor(_client.user!);
    if (!permissions?.has(PermissionFlagsBits.Connect)) {
      return _interaction.editReply("I need permission to **connect** to your voice channel!");
    }
    if (!permissions?.has(PermissionFlagsBits.Speak)) {
      return _interaction.editReply("I need permission to **speak** in your voice channel!");
    }
    if (currentVoiceChannel.type !== ChannelType.GuildVoice) {
      return _interaction.editReply("I can only join standard voice channels.");
    }

    // Get Shoukaku instance
    const shoukaku = _client.shoukaku;
    if (!shoukaku) {
      logger.error("Shoukaku instance not found on _client object!");
      return _interaction.editReply("The music player is not ready yet. Please try again shortly.");
    }

    let player: GuildPlayer | undefined; // Declare player variable outside try block

    try {
      // 3. Get or create player/connection
      player = shoukaku.players.get(_interaction.guildId) as GuildPlayer | undefined;
      const connection = shoukaku.connections.get(_interaction.guildId);

      if (!player || !connection || connection.channelId !== currentVoiceChannel.id) {
        // If player/connection doesn't exist or bot is in wrong channel, join/move
        try {
          player = (await shoukaku.joinVoiceChannel({
            guildId: _interaction.guildId,
            channelId: currentVoiceChannel.id,
            shardId: _interaction.guild.shardId,
          })) as GuildPlayer; // Cast to extended type
          logger.info(
            `Joined/Moved to voice channel ${currentVoiceChannel.name} (${currentVoiceChannel.id}) for play command.`,
          );
          // Initialize queue if it's a new player
          if (!player.queue) {
            player.queue = [];
          }
          player.textChannelId = _interaction.channelId; // Store text channel context
        } catch (joinError: unknown) {
          const errorMsg = joinError instanceof Error ? joinError.message : String(joinError);
          logger.error(
            `Failed to join/move player for guild ${_interaction.guildId}: ${errorMsg}`,
            joinError,
          );
          shoukaku.leaveVoiceChannel(_interaction.guildId).catch(() => {}); // Attempt cleanup
          return _interaction.editReply(
            "An error occurred while trying to join the voice channel.",
          );
        }
      } else {
        // Ensure queue exists if player was retrieved
        if (!player.queue) {
          player.queue = [];
        }
        // Update text channel context if needed
        player.textChannelId = _interaction.channelId;
      }

      // 4. Determine search identifier based on query and source
      let identifier: string;
      const isUrl = query.startsWith("http://") || query.startsWith("https://");

      if (isUrl) {
        identifier = query; // Use URL directly
      } else {
        // Prepend search prefix based on source or default
        switch (source) {
          case "youtube":
            identifier = `ytsearch:${query}`;
            break;
          case "soundcloud":
            identifier = `scsearch:${query}`;
            break;
          case "youtubemusic":
          default: // Default to YouTube Music
            identifier = `ytmsearch:${query}`;
            break;
        }
      }
      logger.debug(`Constructed identifier: ${identifier}`);

      // 5. Search for tracks using Lavalink REST API via an ideal node
      const node = shoukaku.getIdealNode();
      if (!node) {
        throw new Error("No available Lavalink node.");
      }

      // Use the correct return type (LavalinkResponse) and check for undefined
      const searchResult: LavalinkResponse | undefined = await node.rest.resolve(identifier);

      if (!searchResult) {
        throw new Error("REST resolve returned undefined or null.");
      }

      // 6. Process search results and add to queue
      const responseEmbed = new EmbedBuilder().setColor("#0099ff");
      let tracksToAdd: TrackWithRequester[] = [];

      // Switch using string literals based on Lavalink V4 load types
      switch (searchResult.loadType) {
        case "track": {
          // Use 'track'
          const track = searchResult.data as Track;
          // Ensure track and encoded exist before pushing
          if (!track?.encoded) throw new Error("Loaded track is missing encoded data.");
          tracksToAdd.push({
            ...track,
            encoded: track.encoded, // Explicitly include non-null encoded
            requester: { id: _interaction.user.id, tag: _interaction.user.tag },
          });
          responseEmbed
            .setTitle("Track Added to Queue")
            .setDescription(`[${track.info.title}](${track.info.uri})`)
            // Ensure player exists before accessing queue
            .addFields({
              name: "Position in queue",
              value: `${player.queue.length + 1}`,
              inline: true,
            });
          if (track.info.artworkUrl) responseEmbed.setThumbnail(track.info.artworkUrl); // Use artworkUrl
          logger.info(`Adding track: ${track.info.title} (Guild: ${_interaction.guildId})`);
          break;
        }
        case "search": {
          // Use 'search'
          const tracks = searchResult.data as Track[]; // Data is an array of tracks
          if (!tracks || tracks.length === 0) throw new Error("Search returned no results.");
          // Fix: Assign track AFTER the check
          const track = tracks[0];
          if (!track?.encoded) throw new Error("Searched track is missing encoded data.");
          tracksToAdd.push({
            ...track,
            encoded: track.encoded, // Explicitly include non-null encoded
            requester: { id: _interaction.user.id, tag: _interaction.user.tag },
          });
          responseEmbed
            .setTitle("Track Added to Queue")
            .setDescription(`[${track.info.title}](${track.info.uri})`)
            .addFields({
              name: "Position in queue",
              value: `${player.queue.length + 1}`,
              inline: true,
            });
          if (track.info.artworkUrl) responseEmbed.setThumbnail(track.info.artworkUrl);
          logger.info(
            `Adding track from search: ${track.info.title} (Guild: ${_interaction.guildId})`,
          );
          break;
        }
        case "playlist": {
          // Use 'playlist'
          const playlistData = searchResult.data as LavalinkPlaylistData; // Cast to correct structure
          const playlistInfo = playlistData.info;
          const playlistTracks = playlistData.tracks;
          // Fix: Filter out tracks without encoded string and assert non-null for map
          tracksToAdd = playlistTracks
            .filter((track) => !!track.encoded) // Ensure encoded exists
            .map((track) => ({
              ...track,
              encoded: track.encoded!, // Add non-null assertion
              requester: { id: _interaction.user.id, tag: _interaction.user.tag },
            }));
          if (tracksToAdd.length === 0) throw new Error("Playlist contained no playable tracks.");
          // Fix: Use direct optional chaining on array access
          responseEmbed
            .setTitle("Playlist Added to Queue")
            .setDescription(
              `**[${playlistInfo.name}](${identifier})** (${tracksToAdd.length} tracks)`,
            ) // Use filtered length
            .addFields({
              name: "Starting track",
              value: `[${tracksToAdd[0]?.info?.title}](${tracksToAdd[0]?.info?.uri})`,
            }); // Use direct optional chaining
          logger.info(
            `Adding playlist: ${playlistInfo.name} (${tracksToAdd.length} tracks) (Guild: ${_interaction.guildId})`,
          );
          break;
        }
        case "empty": // Use 'empty'
          await _interaction.editReply(`No results found for "${query}".`);
          // Optional: Leave if queue is empty?
          // if (player && !player.track && player.queue.length === 0) {
          //     await shoukaku.leaveVoiceChannel(_interaction.guildId);
          // }
          return; // Stop execution
        case "error": {
          // Use 'error'
          const errorData = searchResult.data as LavalinkErrorData; // Cast to error structure
          // Fix: Add explicit check for errorData
          if (errorData) {
            logger.error(
              `Failed to load track/playlist: ${errorData.message || "Unknown reason"} (Severity: ${errorData.severity || "Unknown"}, Identifier: ${identifier})`,
            );
            await _interaction.editReply(
              `Failed to load track/playlist. Reason: ${errorData.message || "Unknown error"}`,
            );
          } else {
            logger.error(
              `Failed to load track/playlist: Unknown error (Identifier: ${identifier})`,
            );
            await _interaction.editReply(`Failed to load track/playlist. Unknown error.`);
          }
          return; // Stop execution
        }
        default:
          // Use exhaustive check pattern (will error if a case is missed)
          const _exhaustiveCheck: never = searchResult.loadType;
          logger.error(`Unknown loadType received: ${searchResult.loadType}`);
          await _interaction.editReply("Received an unknown response type from the music server.");
          return;
      }

      // Add tracks to the player's queue (ensure player exists)
      if (!player) {
        // This case should ideally not happen if join logic is correct, but added as safeguard
        throw new Error("Player is not defined after processing search results.");
      }
      player.queue.push(...tracksToAdd);

      // Send confirmation embed
      await _interaction.editReply({ embeds: [responseEmbed] });

      // 7. Start playback if not already playing
      await playNext(player, _interaction);
    } catch (error: unknown) {
      // Catch errors during the process
      const errorMsg = error instanceof Error ? error.message : String(error);
      logger.error(
        `Error in play command for query "${query}" in guild ${_interaction.guildId}: ${errorMsg}`,
        error,
      );
      // Use editReply as _interaction is deferred
      await _interaction
        .editReply("An unexpected error occurred while trying to play the music.")
        .catch((e: unknown) => {
          const replyErrorMsg = e instanceof Error ? e.message : String(e);
          logger.error(`Failed to send error reply for play command: ${replyErrorMsg}`);
        });
      // Optional: Attempt to leave VC on critical error?
      // if (shoukaku.players.has(_interaction.guildId)) {
      //     await shoukaku.leaveVoiceChannel(_interaction.guildId).catch(() => {});
      // }
    }
  },
};
