use log::{debug, error, info, warn};
use reqwest::blocking::Client;
use reqwest::header::{HeaderMap, HeaderValue};
use serde::{Deserialize, de::DeserializeOwned};
use serde_json::Value;
use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

use crate::lyrics_api::Response;
use crate::lyrics_api::response::ResponseError;
use crate::metadata::TrackMetadata;

// A huge lines of code borrowed from: https://github.com/Marekkon5/onetagger/blob/0654131188c4df2b4b171ded7cdb927a4369746e/crates/onetagger-platforms/src/musixmatch.rs

#[derive(Clone)]
pub struct MusixmatchAPI {
    client: Client,
    token: Option<String>,
}

#[derive(Debug, Clone, Deserialize)]
struct MxmResponse<B> {
    message: MxmHeader<B>,
}

#[derive(Debug, Clone, Deserialize)]
struct MxmHeader<B> {
    header: MxmHeaderInner,
    body: Option<B>,
}

#[derive(Debug, Clone, Deserialize)]
struct MxmHeaderInner {
    status_code: i32,
}

#[derive(Debug, Clone, Deserialize)]
struct MxmMacroCallsBody<B> {
    macro_calls: HashMap<String, MxmResponse<B>>,
}

#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum MxmBody {
    Richsync {
        richsync: MxmRichsync,
    },
    #[allow(dead_code)]
    Other(Value),
}

#[derive(Debug, Clone, Deserialize)]
struct MxmRichsync {
    richsync_body: String,
}

#[derive(Debug, Clone, Deserialize)]
struct RichsyncLine {
    ts: f32,
    x: String,
    l: Vec<RichsyncWord>,
}

#[derive(Debug, Clone, Deserialize)]
struct RichsyncWord {
    c: String,
    o: f32,
}

pub fn fetch_musixmatch(metadata: &TrackMetadata) -> Result<Response, ResponseError> {
    let mut mxm = MusixmatchAPI::new();
    mxm.fetch(metadata)
}

impl MusixmatchAPI {
    pub fn new() -> Self {
        let mut headers = HeaderMap::new();
        headers.insert(
            "authority",
            HeaderValue::from_static("apic-desktop.musixmatch.com"),
        );
        headers.insert("cookie", HeaderValue::from_static("AWSELBCORS=0; AWSELB=0"));

        MusixmatchAPI {
            client: Client::builder()
                .default_headers(headers)
                .user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36")
                .build()
                .map_err(|e| format!("Failed to build HTTP client: {e}")).unwrap(),
                token: None,
            }
    }

    pub fn fetch(&mut self, metadata: &TrackMetadata) -> Result<Response, ResponseError> {
        let title = metadata.title();
        let artist = metadata.artist().unwrap_or("");

        let macro_calls = self
            .fetch_lyrics(title, artist, 0)
            .map_err(|e| ResponseError::new(format!("Musixmatch error: {e}")))?;

        if let Some(response) = self.try_richsync(&macro_calls) {
            info!(
                "Searching word-level lyrics format for {} from musixmatch",
                metadata.path().display()
            );
            return Ok(response);
        }
        Ok(Response::None)
    }

    fn try_richsync(
        &self,
        macro_calls: &HashMap<String, MxmResponse<MxmBody>>,
    ) -> Option<Response> {
        let rich = macro_calls.get("track.richsync.get")?;
        let body = rich.message.body.as_ref()?;

        if let MxmBody::Richsync { richsync } = body {
            let lines: Vec<RichsyncLine> = serde_json::from_str(&richsync.richsync_body).ok()?;

            if lines.is_empty() {
                return None;
            }

            let synced = Self::richsync_to_enhanced_lrc(&lines);
            let plain = lines
                .iter()
                .map(|l| l.x.as_str())
                .collect::<Vec<_>>()
                .join("\n");

            Some(Response::SyncedLyrics { synced, plain })
        } else {
            None
        }
    }

    fn richsync_to_enhanced_lrc(lines: &[RichsyncLine]) -> String {
        lines
            .iter()
            .map(|line| {
                let line_ts = Self::format_lrc_time(line.ts);

                let words: String = line
                    .l
                    .iter()
                    .map(|word| {
                        let word_ts = Self::format_lrc_time(line.ts + word.o);
                        format!("<{}> {}", word_ts, word.c)
                    })
                    .collect::<Vec<_>>()
                    .join("");

                format!("[{}]{}", line_ts, words)
            })
            .collect::<Vec<_>>()
            .join("\n")
    }

    fn format_lrc_time(secs: f32) -> String {
        let minutes = (secs / 60.0) as u32;
        let remaining = secs % 60.0;
        format!("{:02}:{:05.2}", minutes, remaining)
    }

    fn fetch_token(&mut self) -> Result<(), Box<dyn std::error::Error>> {
        debug!("Fetching Musixmatch token");
        let t: Value = self.get("token.get", &[("user_language", "en")])?;

        if t["message"]["header"]["status_code"].as_i64() == Some(401) {
            warn!("Musixmatch captcha when getting token! Waiting 10s...");
            std::thread::sleep(Duration::from_secs(10));
            return self.fetch_token();
        }

        let token = t["message"]["body"]["user_token"]
            .as_str()
            .ok_or("Couldn't fetch token")?
            .to_string();
        self.token = Some(token);
        Ok(())
    }

    fn get<O: DeserializeOwned>(
        &mut self,
        action: &str,
        query: &[(&str, &str)],
    ) -> Result<O, Box<dyn std::error::Error>> {
        if action != "token.get" && self.token.is_none() {
            self.fetch_token()?;
        }

        debug!("Musixmatch GET: {action}");
        let mut query = query.to_vec();
        query.push(("app_id", "web-desktop-app-v1.0"));

        let token = self.token.clone();
        if let Some(ref token) = token {
            query.push(("usertoken", token));
        }

        let t = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_millis()
            .to_string();
        query.push(("t", &t));

        let url = format!("https://apic-desktop.musixmatch.com/ws/1.1/{action}");
        let resp = self.client.get(&url).query(&query).send()?.json()?;
        Ok(resp)
    }

    fn fetch_lyrics(
        &mut self,
        title: &str,
        artist: &str,
        retry_count: u32,
    ) -> Result<HashMap<String, MxmResponse<MxmBody>>, Box<dyn std::error::Error>> {
        let r: MxmResponse<MxmMacroCallsBody<MxmBody>> = self.get(
            "macro.subtitles.get",
            &[
                ("format", "json"),
                ("namespace", "lyrics_richsynced"),
                ("optional_calls", "track.richsync"),
                ("subtitle_format", "lrc"),
                ("q_artist", artist),
                ("q_track", title),
            ],
        )?;

        if r.message.header.status_code == 401 {
            if retry_count >= 3 {
                error!("Musixmatch captcha: too many retries!");
                self.fetch_token()?;
                return Err("Too many retries!".into());
            }
            return self.fetch_lyrics(title, artist, retry_count + 1);
        }

        let body = r.message.body.ok_or("Missing response body")?;
        Ok(body.macro_calls)
    }
}
