use super::{
  is_valid_url,
  join_voice_channel,
  leave_voice_channel,
  queue::{TrackInfo, enqueue_tracks, pop_front},
};
use crate::{
  chat::{create_message_response, edit_response, process_message},
  error::{Error, Result},
  handler::{get_client, get_songbird},
  ytdlp::{YtDlp, YtDlpItem},
};
use serenity::{
  all::{CommandInteraction, CommandOptionType, CreateCommand, CreateCommandOption},
  async_trait,
  model::prelude::GuildId,
  prelude::*,
};
use siren_core::data::guilds::GuildCache;
use songbird::{
  Event,
  EventHandler,
  Songbird,
  TrackEvent,
  input::{Input, YoutubeDl},
  tracks::TrackHandle,
};
use std::sync::Arc;

pub async fn run(ctx: &Context, command: &CommandInteraction) {
  // Process the command options
  let track_url = match command.data.options.first() {
    Some(o) => o.value.as_str().unwrap(),
    None => {
      log::warn!(
        "<{}> {} attempted to play a track without a track option",
        command.guild_id.unwrap(),
        command.user.id.get()
      );
      create_message_response(ctx, command, "Track option is missing".to_string(), false).await;
      return;
    }
  };

  // Create the initial response
  process_message(ctx, command, false).await;

  // Get the songbird manager
  let manager = get_songbird();

  // Extract the guild ID
  let guild_id = match &command.guild_id {
    Some(guild_id) => guild_id,
    None => {
      edit_response(
        ctx,
        command,
        "Unable to find the current server ID".to_string(),
      )
      .await;
      return;
    }
  };

  // Join the user's voice channel
  match join_voice_channel(&ctx.cache, manager, guild_id, &command.user.id).await {
    Ok(channel_id) => {
      log::debug!(
        "<{guild_id}> Play command executed on channel {channel_id} with track: {track_url:?}"
      );
      // Handle the track url
      match enqueue_track(manager, guild_id.to_owned(), track_url).await {
        Ok(items) => {
          let mut message = format!("Added {} tracks", items.len());
          if items.is_empty() {
            message = "No tracks were played".to_string();
            log::warn!("<{guild_id}> No tracks were played");
            if let Err(err) = leave_voice_channel(manager, guild_id).await {
              log::error!("Failed to leave voice channel: {}", err);
            };
          } else if items.len() == 1 {
            message = format!("Added **{}**", items[0].get_title());
          }
          edit_response(ctx, command, message).await;
        }
        Err(err) => {
          log::error!("Failed to play track: {}", err);
          if let Err(err) = leave_voice_channel(manager, guild_id).await {
            log::error!("Failed to leave voice channel: {}", err);
          }
          edit_response(ctx, command, format!("Failed to play track: {}", err)).await;
        }
      };
    }
    Err(err) => {
      log::warn!("<{guild_id}> Failed to join voice channel: {}", err);
      edit_response(ctx, command, format!("{}", err)).await;
    }
  }
}

pub async fn enqueue_track(
  manager: &Arc<Songbird>,
  guild_id: GuildId,
  track_url: &str,
) -> Result<Vec<YtDlpItem>> {
  let mut playlist_items: Vec<YtDlpItem> = Vec::new();
  if let Some(handler_lock) = manager.get(guild_id) {
    let mut handler = handler_lock.lock().await;
    let guild = GuildCache::find_by_id(guild_id.get() as i64).await.unwrap();
    let valid = is_valid_url(track_url);

    // Check if the URL is valid
    if !valid {
      log::warn!("<{guild_id}> Invalid track url: {}", track_url);
      return Err(Error::new(422, format!("Invalid track url: {}", track_url)));
    }

    playlist_items = get_ytdlp_items(track_url)?;

    // Collect TrackInfo for the queue store before borrowing `item` in the loop
    let track_infos: Vec<TrackInfo> = playlist_items
      .iter()
      .map(|item| TrackInfo {
        title: item.get_title().to_owned(),
        url: item.get_url().to_owned(),
      })
      .collect();

    // Add each track to the queue
    for item in &playlist_items {
      let volume = guild.volume as f32 / 100.0;
      let http_client = get_client();

      let source = YoutubeDl::new(http_client.to_owned(), item.get_url().to_owned());
      let input: Input = source.into();
      let track_title = item.get_title().to_owned();

      let track_handle: TrackHandle = handler.enqueue_input(input).await;

      // Set the volume
      let _ = track_handle.set_volume(volume);

      log::debug!("<{guild_id}> Added track: {}", track_title);
      handler.remove_all_global_events();
      handler.add_global_event(
        Event::Track(TrackEvent::End),
        TrackEndNotifier {
          guild_id,
          call: manager.clone(),
        },
      );
    }

    // Store track metadata so the REST API can expose queue info
    enqueue_tracks(guild_id.get(), track_infos);

    if handler.queue().is_empty() {
      let _ = handler.queue().resume();
    }
  }
  Ok(playlist_items)
}

pub fn get_ytdlp_items(url: &str) -> Result<Vec<YtDlpItem>> {
  let output = YtDlp::new()
    .arg("--flat-playlist")
    .arg("--dump-json")
    .arg("--no-check-formats")
    .arg(url)
    .execute()?;

  // Check if yt-dlp exited successfully; log stderr if not
  if !output.status.success() {
    let stderr = String::from_utf8_lossy(&output.stderr);
    return Err(Error::new(
      500,
      format!("yt-dlp failed ({}): {}", output.status, stderr.trim()),
    ));
  }

  let stdout = String::from_utf8(output.stdout)?;

  let items: Vec<YtDlpItem> = stdout
    .split('\n')
    .filter_map(|line| {
      if line.is_empty() {
        None
      } else {
        Some(
          serde_json::from_slice::<YtDlpItem>(line.as_bytes())
            .map_err(|err| Error::new(500, err.to_string())),
        )
      }
    })
    .filter_map(|parsed| match parsed {
      Ok(item) => Some(item),
      Err(err) => {
        log::warn!("Failed to parse yt-dlp item: {}", err);
        None
      }
    })
    .collect();
  Ok(items)
}

pub fn register() -> CreateCommand {
  CreateCommand::new("play")
    .description("Plays the given track")
    .add_option(
      CreateCommandOption::new(CommandOptionType::String, "track", "The track to be played")
        .required(true),
    )
}

struct TrackEndNotifier {
  pub call: Arc<Songbird>,
  pub guild_id: GuildId,
}

#[async_trait]
impl EventHandler for TrackEndNotifier {
  async fn act(&self, ctx: &songbird::events::EventContext<'_>) -> Option<songbird::events::Event> {
    if let songbird::EventContext::Track(_track_list) = ctx {
      // Remove the finished track from our metadata store
      pop_front(self.guild_id.get());

      if let Some(call) = self.call.get(self.guild_id) {
        let mut handler = call.lock().await;
        if handler.queue().is_empty() {
          log::debug!("Queue is empty, leaving voice channel");
          handler.leave().await.unwrap();
        }
      }
    }
    None
  }
}
