use std::env;
use std::sync::Arc;
use std::collections::HashMap;
use std::time::Duration;

use tokio::time::sleep;

use opensubsonic::Auth;
use opensubsonic::Client as SubsonicClient;

use reqwest::Client as HttpClient;

use serenity::async_trait;
use serenity::http::Http;
use serenity::Result as SerenityResult;
use serenity::model::application::ComponentInteractionDataKind;
use serenity::model::channel::Message;
use serenity::model::prelude::ChannelId;
use serenity::model::prelude::GuildId;
use serenity::model::gateway::Ready;
use serenity::prelude::*;

use songbird::SerenityInit;
use songbird::events::{Event, EventContext, EventHandler as VoiceEventHandler, TrackEvent};

use songbird::input::HttpRequest as SongbirdHttp;
use songbird::tracks::{Track, TrackHandle, PlayMode};

use poise::serenity_prelude as serenity;
use poise::reply::ReplyHandle;

// poise crud
struct CommandData {
    subsonic_client: Arc<SubsonicClient>,
}
type Error = Box<dyn std::error::Error + Send + Sync>;
type CommandContext<'a> = poise::Context<'a, CommandData, Error>;

struct Handler;

// https://stackoverflow.com/a/38461750
fn truncate(s: &str, max_chars: usize) -> &str {
    match s.char_indices().nth(max_chars) {
        None => s,
        Some((idx, _)) => &s[..idx],
    }
}

fn truncate_in_place(s: &mut String, max_chars: usize) {
    let bytes = truncate(&s, max_chars).len();
    s.truncate(bytes);
}

fn duration_string(duration: Duration) -> String {
    if !duration.is_zero() {
        let duration_minutes = duration.as_secs() / 60;
        let duration_seconds = duration.as_secs() % 60;
        return format!("{}:{:02}", duration_minutes, duration_seconds)
     } else {
         return "0:00".to_string()
     };
}

fn get_description(track: &TrackHandle) -> String {
    let user_data: Arc<HashMap<String, String>> = track.data();
    return user_data.get("description").unwrap().clone();
}

/// Registers the bot's commands on Discord.
#[poise::command(prefix_command)]
pub async fn register(ctx: CommandContext<'_>) -> Result<(), Error> {
    poise::builtins::register_application_commands_buttons(ctx).await?;
    Ok(())
}


/// Join a voice channel that you're in.
#[poise::command(slash_command)]
async fn join(ctx: CommandContext<'_>) -> Result<(), Error> {
    let (guild_id, channel_id) = {
        let guild = ctx.guild().unwrap();
        let channel_id = guild
            .voice_states
            .get(&ctx.author().id)
            .and_then(|voice_state| voice_state.channel_id);

        (guild.id, channel_id)
    };

    let connect_to = match channel_id {
        Some(channel) => channel,
        None => {
            poise_check_msg(ctx.reply("Not in a voice channel").await);
            return Ok(());
        },
    };

    let manager = songbird::get(ctx.serenity_context())
        .await
        .expect("Songbird Voice client placed in at initialisation.")
        .clone();

    if let Ok(handler_lock) = manager.join(guild_id, connect_to).await {
        let mut handle = handler_lock.lock().await;
        let chan_id = ctx.channel_id();

        let send_http = ctx.serenity_context().http.clone();
        handle.add_global_event(
            Event::Track(TrackEvent::End),
            TrackEndNotifier {
                chan_id,
                http: send_http,
            },
        );
        let send_http = ctx.serenity_context().http.clone();
        handle.add_global_event(
            Event::Track(TrackEvent::Play),
            TrackPlayNotifier {
                guild_id,
                chan_id,
                ctx: ctx.serenity_context().clone(), // ugh
                http: send_http,
            },
        );
        
        poise_check_msg(ctx.reply("Joined voice channel").await);
    }
    return Ok(());
}

/// Leave the voice channel that the bot is in.
#[poise::command(slash_command)]
async fn leave(ctx: CommandContext<'_>) -> Result<(), Error> {
    let guild_id = ctx.guild_id().unwrap();

    let manager = songbird::get(ctx.serenity_context())
        .await
        .expect("Songbird Voice client placed in at initialisation.")
        .clone();
    let has_handler = manager.get(guild_id).is_some();

    if has_handler {
        if let Err(e) = manager.remove(guild_id).await {
            poise_check_msg(ctx.reply(format!("Failed: {:?}", e))
                    .await,
            );
        }

        poise_check_msg(ctx.reply("Left voice channel").await);
    } else {
        poise_check_msg(ctx.reply("Not in a voice channel").await);
    }
    return Ok(());
}

/// Print the current queue of tracks to the channel.
#[poise::command(slash_command)]
async fn print_queue(ctx: CommandContext<'_>) -> Result<(), Error> {
    let guild_id = ctx.guild_id().unwrap();

    let manager = songbird::get(ctx.serenity_context())
        .await
        .expect("Songbird Voice client placed in at initialisation.")
        .clone();

    if let Some(handler_lock) = manager.get(guild_id) {
        let handler = handler_lock.lock().await;
        let queue = handler.queue().current_queue();
        if queue.len() > 0 {
            let mut output = String::new();
            for track in queue {
                let extra = if let Some(current_track) = handler.queue().current()
                        && current_track.uuid() == track.uuid()
                        && let Ok(info) = current_track.get_info().await {
                    let state_string = match info.playing {
                        PlayMode::Play => "playing",
                        PlayMode::Pause => "paused",
                        PlayMode::Stop => "stopped",
                        PlayMode::End => "ended",
                        PlayMode::Errored(_) => "error",
                        _ => "unknown"
                    };
                    let duration = duration_string(info.position);
                    
                    let user_data: Arc<HashMap<String, String>> = current_track.data();
                    let length = user_data.get("duration").unwrap().clone();
                    format!(" ({}, {}/{})", state_string, duration, length)
                } else {
                    "".to_string()
                };
                output = format!("{}* {}{}\n", output, get_description(&track), extra);
            }
            poise_check_msg(ctx.reply(output)
                    .await,
            );
        } else {
            poise_check_msg(ctx.reply("No tracks queued.")
                    .await,
            );
        }
    } else {
        poise_check_msg(ctx.reply("Not in a voice channel to play in")
                .await,
        );
    }
    return Ok(());
}

/// Resumes playback when paused.
#[poise::command(slash_command)]
async fn play(ctx: CommandContext<'_>) -> Result<(), Error> {
    let guild_id = ctx.guild_id().unwrap();

    let manager = songbird::get(ctx.serenity_context())
        .await
        .expect("Songbird Voice client placed in at initialisation.")
        .clone();

    if let Some(handler_lock) = manager.get(guild_id) {
        let handler = handler_lock.lock().await;
        let queue = handler.queue();
        if queue.current().is_none() {
            poise_check_msg(ctx.reply("Nothing to resume. Queue something up!")
                    .await,
            );
            return Ok(());
        }
        match queue.resume() {
            Ok(_) => poise_check_msg(ctx.reply("Track resumed.").await),
            Err(err) => {
                println!("Resume error: {}", err);
                poise_check_msg(ctx.reply("Error resuming.").await)
            },
        }
    } else {
        poise_check_msg(ctx.reply("Not in a voice channel to play in")
                .await,
        );
    }
    return Ok(());
}

/// Pauses playback.
#[poise::command(slash_command)]
async fn pause(ctx: CommandContext<'_>) -> Result<(), Error> {
    let guild_id = ctx.guild_id().unwrap();

    let manager = songbird::get(ctx.serenity_context())
        .await
        .expect("Songbird Voice client placed in at initialisation.")
        .clone();

    if let Some(handler_lock) = manager.get(guild_id) {
        let handler = handler_lock.lock().await;
        let queue = handler.queue();
        if queue.current().is_none() {
            poise_check_msg(ctx.reply("Nothing to pause. Queue something up!")
                    .await,
            );
            return Ok(());
        }
        match queue.pause() {
            Ok(_) => poise_check_msg(ctx.reply("Track paused.").await),
            Err(err) => {
                println!("Pause error: {}", err);
                poise_check_msg(ctx.reply("Error pausing.").await)
            },
        }
    } else {
        poise_check_msg(ctx.reply("Not in a voice channel to play in")
                .await,
        );
    }
    return Ok(());
}

/// Stops playback.
#[poise::command(slash_command)]
async fn stop(ctx: CommandContext<'_>) -> Result<(), Error> {
    let guild_id = ctx.guild_id().unwrap();

    let manager = songbird::get(ctx.serenity_context())
        .await
        .expect("Songbird Voice client placed in at initialisation.")
        .clone();

    if let Some(handler_lock) = manager.get(guild_id) {
        let handler = handler_lock.lock().await;
        let queue = handler.queue();
        queue.stop();

        poise_check_msg(ctx.reply("Queue cleared.").await);
    } else {
        poise_check_msg(ctx.reply("Not in a voice channel to play in")
                .await,
        );
    }
    return Ok(());
}

/// Skips the current track and moves to the next one in the queue.
#[poise::command(slash_command)]
async fn skip(ctx: CommandContext<'_>) -> Result<(), Error> {
    let guild_id = ctx.guild_id().unwrap();

    let manager = songbird::get(ctx.serenity_context())
        .await
        .expect("Songbird Voice client placed in at initialisation.")
        .clone();

    if let Some(handler_lock) = manager.get(guild_id) {
        let handler = handler_lock.lock().await;
        let queue = handler.queue();
        let _ = queue.skip();

        poise_check_msg(ctx.reply(format!("Song skipped: {} in queue.", queue.len()),
                )
                .await,
        );
    } else {
        poise_check_msg(ctx.reply("Not in a voice channel to play in")
                .await,
        );
    }
    return Ok(());
}

/// Mutes the bot in the voice channel.
#[poise::command(slash_command)]
async fn mute(ctx: CommandContext<'_>) -> Result<(), Error> {
    let guild_id = ctx.guild_id().unwrap();

    let manager = songbird::get(ctx.serenity_context())
        .await
        .expect("Songbird Voice client placed in at initialisation.")
        .clone();

    let handler_lock = match manager.get(guild_id) {
        Some(handler) => handler,
        None => {
            poise_check_msg(ctx.reply("Not in a voice channel").await);

            return Ok(());
        },
    };

    let mut handler = handler_lock.lock().await;

    if handler.is_mute() {
        poise_check_msg(ctx.reply("Already muted").await);
    } else {
        if let Err(e) = handler.mute(true).await {
            poise_check_msg(ctx.reply(format!("Failed: {:?}", e))
                    .await,
            );
        }

        poise_check_msg(ctx.reply("Now muted").await);
    }
    return Ok(());
}

/// Unmutes in the voice channel if muted.
#[poise::command(slash_command)]
async fn unmute(ctx: CommandContext<'_>) -> Result<(), Error> {
    let guild_id = ctx.guild_id().unwrap();

    let manager = songbird::get(ctx.serenity_context())
        .await
        .expect("Songbird Voice client placed in at initialisation.")
        .clone();

    if let Some(handler_lock) = manager.get(guild_id) {
        let mut handler = handler_lock.lock().await;
        if let Err(e) = handler.mute(false).await {
            poise_check_msg(ctx.reply(format!("Failed: {:?}", e))
                    .await,
            );
        }

        poise_check_msg(ctx.reply("Unmuted").await);
    } else {
        poise_check_msg(ctx.reply("Not in a voice channel to unmute in")
                .await,
        );
    }
    return Ok(());
}

struct SubsonicSearchTrackResult {
    title: String,
    artist: String,
    album: String,
    duration: Duration,
    url: String
}

impl SubsonicSearchTrackResult {
    fn label(&self) -> String {
        return format!("'{}', by '{}', from '{}'", self.title, self.artist, self.album);
    }
    
    fn label_truncated(&self, max_chars: usize) -> String {
        let mut label = self.label();
        if label.len() > max_chars {
            truncate_in_place(&mut label, max_chars);
        }
        return label;
    }
}

async fn play_url(ctx: CommandContext<'_>, result: &SubsonicSearchTrackResult) {
    let guild_id = ctx.guild_id().unwrap();
    
    let manager = songbird::get(ctx.serenity_context())
        .await
        .expect("Songbird Voice client placed in at initialisation.")
        .clone();
    if let Some(handler_lock) = manager.get(guild_id) {
        let mut handler = handler_lock.lock().await;
        let http_client = HttpClient::new();
        // XXX: Reusable HTTP client, like in
        // https://raw.githubusercontent.com/serenity-rs/songbird/refs/heads/current/examples/serenity/voice_events_queue/src/main.rs
        let src = SongbirdHttp::new(http_client, result.url.clone());
        
        let mut user_data = Arc::new(HashMap::new());
        let description = result.label();
        
        let duration_string = duration_string(result.duration);
        
        Arc::make_mut(&mut user_data)
            .insert("description".to_string(), description);
        Arc::make_mut(&mut user_data)
            .insert("duration".to_string(), duration_string);
        
        let track = Track::new_with_data(src.into(), user_data);
        handler.enqueue(track).await;
    
        poise_check_msg(ctx.reply(
                    format!("Added song '{}' to queue: position {}", result.title, handler.queue().len()),
                )
                .await,
        );
    } else {
        poise_check_msg(ctx.reply("Not in a voice channel to play in")
                .await,
        );
    }
}

fn parse_track_results(client: &Arc<opensubsonic::Client>, tracks: &Vec<opensubsonic::data::Child>) -> HashMap<String, SubsonicSearchTrackResult> {
    let mut parsed_results: HashMap<String, SubsonicSearchTrackResult> = HashMap::new();
    
    for result in tracks {
        // implicit discord limit?
        if parsed_results.len() >= 25 {
            break;
        }
        let title = result.title.clone();
        let artist = result.artist.as_ref().unwrap().clone();
        let album = result.album.as_ref().unwrap().clone();
        let duration_secs = result.duration
            .unwrap_or(0).try_into().expect("Hey, where's my duration?");
        let duration = Duration::from_secs(duration_secs);
        // set channel bitrate here if we want to transcode
        let url: String = client.stream_url(result.id.as_ref(), None, None).unwrap().into();
        parsed_results.insert(result.id.clone(), SubsonicSearchTrackResult {
            title, artist, album, duration, url
        });
    }
    
    return parsed_results;
}

fn components_from_track_results(tracks: &HashMap<String, SubsonicSearchTrackResult>) -> Vec<serenity::CreateActionRow>
{
    if tracks.len() == 1 {
        return vec![
            serenity::CreateActionRow::Buttons(
                tracks.iter().map(|(k, v)| {
                    // truncate to 100 chars, discord limit?
                    let label = v.label_truncated(100);
                    return serenity::CreateButton::new(format!("{k}"))
                        .label(label)
                }).collect()
            )
        ]
    }
    return vec![
        serenity::CreateActionRow::SelectMenu(
            serenity::CreateSelectMenu::new(
                "tracks".to_string(),
                serenity::CreateSelectMenuKind::String {
                    options: tracks.iter().map(|(k, v)| {
                        let label = v.label_truncated(100);
                        return serenity::CreateSelectMenuOption::new(label, format!("{k}"))
                    }).collect()
                }
            )
        )
    ];
}

async fn track_select_interaction(ctx: CommandContext<'_>, tracks: &HashMap<String, SubsonicSearchTrackResult>, message: String) -> Result<(), Error> {
    let create_reply = {
        let components = components_from_track_results(tracks);
        
        poise::CreateReply::default()
            .content(message)
            .components(components)
    };
    
    let reply = ctx.send(create_reply).await?;
    let message = reply.message().await?;
    let interaction = match message
        .await_component_interaction(&ctx.serenity_context().shard)
        .timeout(Duration::from_secs(60 * 3))
        .await
    {
        Some(x) => x,
        None => {
            message.delete(&ctx).await?;
            return Ok(());
        },
    };
    
    if let Some(track_id) = match &interaction.data.kind {
        ComponentInteractionDataKind::StringSelect {
            values
        } => Some(&values[0]),
        ComponentInteractionDataKind::Button => Some(&interaction.data.custom_id),
        _ => None
    } && let Some(result) = tracks.get(track_id) {
        play_url(ctx, &result).await;
    }
    
    // XXX: Use an interaction stream if we want multiple responses?
    // See: https://github.com/serenity-rs/serenity/blob/current/examples/e17_message_components/src/main.rs
    
    interaction.create_response(ctx, serenity::CreateInteractionResponse::Acknowledge).await?;
    message.delete(&ctx).await?;
    return Ok(());
}

/// Search for tracks from Subsonic.
#[poise::command(slash_command)]
async fn search(ctx: CommandContext<'_>,
        #[description = "Track? Artist? Album? Who knows!"] q: String) -> Result<(), Error> {
    let subsonic_client = &ctx.data().subsonic_client;
    let results = subsonic_client
        .search3(q.as_str(), None, None, None, None, Some(25), None, None)
        .await
        .expect("Failed to search Subsonic server");
        
    if results.song.len() == 0 {
        poise_check_msg(ctx.reply(
                format!("No tracks found for '{}'", q)
            )
            .await
        );
        sleep(Duration::from_secs(60 * 3)).await;
        return Ok(());
    }
    
    let parsed_results = parse_track_results(&subsonic_client, &results.song);
    track_select_interaction(ctx, &parsed_results, format!("Results for '{}'", q)).await?;
    
    return Ok(());
}

/// Get a random set of tracks to pick from.
#[poise::command(slash_command)]
async fn random(ctx: CommandContext<'_>,
        #[description = "Optional, genre (from the ID3 soup)"] genre: Option<String>,
        #[description = "Starting from the year"] from_year: Option<i32>,
        #[description = "Up to the the year"] to_year: Option<i32>) -> Result<(), Error> {
    let genre_as_str = genre.as_deref();
    let subsonic_client = &ctx.data().subsonic_client;
    let results = subsonic_client
        .get_random_songs(Some(25), genre_as_str, from_year, to_year, None)
        .await
        .expect("Failed to get random tracks");

    let parsed_results = parse_track_results(&subsonic_client, &results);
    track_select_interaction(ctx, &parsed_results, "Random charcuterie platter".to_string()).await?;
    
    return Ok(());
}

#[async_trait]
impl EventHandler for Handler {
    async fn ready(&self, _: Context, ready: Ready) {
        println!("{} is connected!", ready.user.name);
    }
}

struct TrackEndNotifier {
    chan_id: ChannelId,
    http: Arc<Http>,
}

#[async_trait]
impl VoiceEventHandler for TrackEndNotifier {
    async fn act(&self, ctx: &EventContext<'_>) -> Option<Event> {
        if let EventContext::Track(track_list) = ctx {
            check_msg(
                self.chan_id
                    .say(&self.http, &format!("Tracks ended: {}.", track_list.len()))
                    .await,
            );
        }

        None
    }
}

struct TrackPlayNotifier {
    guild_id: GuildId,
    chan_id: ChannelId,
    ctx: Context, // clone for now, figure out lifetimes later
    http: Arc<Http>,
}

#[async_trait]
impl VoiceEventHandler for TrackPlayNotifier {
    async fn act(&self, _ctx: &EventContext<'_>) -> Option<Event> {
        let manager = songbird::get(&self.ctx)
            .await
            .expect("Songbird Voice client placed in at initialisation.")
            .clone();

        if let Some(handler_lock) = manager.get(self.guild_id) {
            let handler = handler_lock.lock().await;
            if let Some(next_track) = handler.queue().current() {
                let filename = get_description(&next_track);
                check_msg(
                    self.chan_id
                        .say(&self.http, &format!("Track now playing: {}", filename))
                        .await,
                );
            }
        }
        None
    }
}

#[tokio::main]
async fn main() {
    // XXX: Gross workaround for "Could not automatically determine the process-level CryptoProvider from Rustls crate features"    
    rustls::crypto::ring::default_provider()
        .install_default()
        .expect("Failed to install rustls crypto provider");

    
    // Login with a bot token from the environment
    let token = env::var("DISCORD_TOKEN").expect("Expected a token in the environment");
    let subsonic_server = env::var("SUBSONIC_SERVER").expect("Expected a server in the environment");
    let subsonic_user = env::var("SUBSONIC_USERNAME").expect("Expected a username in the environment");
    let subsonic_pass = env::var("SUBSONIC_PASSWORD").expect("Expected a passord in the environment");
    // Set gateway intents, which decides what events the bot will be notified about
    let intents = GatewayIntents::non_privileged()
        | GatewayIntents::GUILD_MESSAGES
        | GatewayIntents::MESSAGE_CONTENT;

    let subsonic_client = SubsonicClient::new(
        &subsonic_server,
        Auth::token(subsonic_user, subsonic_pass),
    ).expect("Couldn't create client");
    let _ = subsonic_client.ping().await;

    let framework = poise::Framework::builder()
        .options(poise::FrameworkOptions {
            commands: vec![
                register(),
                join(),
                leave(),
                print_queue(),
                play(),
                pause(),
                stop(),
                skip(),
                mute(),
                unmute(),
                search(),
                random(),
                ],
            prefix_options: poise::PrefixFrameworkOptions {
                prefix: Some("!".into()),
                ..Default::default()
            },
            ..Default::default()
        })
        .setup(|ctx, _ready, framework| {
            Box::pin(async move {
                poise::builtins::register_globally(ctx, &framework.options().commands).await?;
                Ok(CommandData {
                    subsonic_client: Arc::new(subsonic_client)
                })
            })
        })
        .build();

    // Create a new instance of the Client, logging in as a bot.
    let mut client = Client::builder(&token, intents)
        .framework(framework)
        .event_handler(Handler)
        .register_songbird()
        .await
        .expect("Err creating client");

    // Start listening for events by starting a single shard
    if let Err(why) = client.start().await {
        println!("Client error: {why:?}");
    }
}

/// Checks that a message successfully sent; if not, then logs why to stdout.
fn check_msg(result: SerenityResult<Message>) {
    if let Err(why) = result {
        println!("Error sending message: {:?}", why);
    }
}

fn poise_check_msg(result: Result<ReplyHandle<'_>, SerenityError>) {
    if let Err(why) = result {
        println!("Error sending message: {:?}", why);
    }
}
