use crate::{
    protocol::tracks::{LoadResult, Track, TrackInfo},
    sources::{SourcePlugin, playable_track::BoxedTrack},
};
use async_trait::async_trait;
use regex::Regex;
use reqwest::header::USER_AGENT;
use serde_json::Value;
use std::sync::{Arc, OnceLock};
use tracing::error;
const SEARCH_URL: &str = "https://www.shazam.com/services/amapi/v1/catalog/US/search";
fn url_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r"https?://(?:www\.)?shazam\.com/song/\d+(?:/[^/?#]+)?")
            .expect("shazam URL regex is a valid literal")
    })
}
fn og_title_regex() -> &'static Regex {
    static REGEX: OnceLock<Regex> = OnceLock::new();
    REGEX.get_or_init(|| {
        Regex::new(r"^(.+?) - (.+?):").expect("shazam og:title regex is a valid literal")
    })
}
pub struct ShazamSource {
    client: Arc<reqwest::Client>,
    search_limit: usize,
}
impl ShazamSource {
    pub fn new(
        config: &crate::config::AppConfig,
        client: Arc<reqwest::Client>,
    ) -> Result<Self, String> {
        Ok(Self {
            client,
            search_limit: config
                .sources
                .shazam
                .as_ref()
                .map(|c| c.search_limit)
                .unwrap_or(10),
        })
    }
    async fn search(&self, query: &str) -> LoadResult {
        let url = format!(
            "{SEARCH_URL}?types=songs&term={query}&limit={limit}",
            query = urlencoding::encode(query),
            limit = self.search_limit
        );
        let resp = match self.base_request(self.client.get(&url)).send().await {
            Ok(r) => r,
            Err(e) => {
                error!("Shazam search request failed: {e}");
                return LoadResult::Empty {};
            }
        };
        if !resp.status().is_success() {
            return LoadResult::Empty {};
        }
        let data: Value = match resp.json().await {
            Ok(v) => v,
            Err(e) => {
                error!("Failed to parse Shazam search JSON: {e}");
                return LoadResult::Empty {};
            }
        };
        let songs = data
            .pointer("/results/songs/data")
            .and_then(|v| v.as_array());
        let Some(songs) = songs else {
            return LoadResult::Empty {};
        };
        let mut tracks = Vec::new();
        for item in songs {
            if let Some(track) = self.build_track(item) {
                tracks.push(track);
            }
        }
        if tracks.is_empty() {
            LoadResult::Empty {}
        } else {
            LoadResult::Search(tracks)
        }
    }
    fn build_track(&self, item: &Value) -> Option<Track> {
        let attributes = item.get("attributes")?;
        let id = item.get("id")?.as_str()?.to_owned();
        let title = attributes
            .get("name")
            .and_then(|v| v.as_str())
            .unwrap_or("Unknown")
            .to_owned();
        let author = attributes
            .get("artistName")
            .and_then(|v| v.as_str())
            .unwrap_or("Unknown")
            .to_owned();
        let length = attributes
            .get("durationInMillis")
            .and_then(|v| v.as_u64())
            .unwrap_or(0);
        let isrc = attributes
            .get("isrc")
            .and_then(|v| v.as_str())
            .map(|s| s.to_owned());
        let artwork_url = attributes
            .get("artwork")
            .and_then(|a| a.get("url"))
            .and_then(|v| v.as_str())
            .map(|s| s.replace("{w}", "1000").replace("{h}", "1000"));
        let uri = attributes
            .get("url")
            .and_then(|v| v.as_str())
            .map(|s| s.to_owned());
        let mut track = Track::new(TrackInfo {
            identifier: id,
            is_seekable: true,
            author,
            length,
            is_stream: false,
            position: 0,
            title,
            uri: uri.clone(),
            artwork_url,
            isrc,
            source_name: "shazam".to_owned(),
        });
        track.plugin_info = serde_json::json!({
            "albumName": null,
            "albumUrl": null,
            "artistUrl": null,
            "artistArtworkUrl": null,
            "previewUrl": null,
            "isPreview": false
        });
        Some(track)
    }
    async fn resolve_url(&self, url: &str) -> LoadResult {
        let resp = match self.base_request(self.client.get(url)).send().await {
            Ok(r) => r,
            Err(e) => {
                error!("Shazam resolve request failed: {e}");
                return LoadResult::Empty {};
            }
        };
        if !resp.status().is_success() {
            return LoadResult::Empty {};
        }
        let html = match resp.text().await {
            Ok(t) => t,
            Err(e) => {
                error!("Failed to read Shazam HTML: {e}");
                return LoadResult::Empty {};
            }
        };
        let title = self.extract_text_after_class(&html, "NewTrackPageHeader_trackTitle__");
        let artist = self.extract_text_after_class(&html, "TrackPageArtistLink_artistNameText__");
        let artwork_url = self.extract_artwork(&html);
        let isrc = self.extract_isrc(&html);
        let duration_ms = self.extract_duration(&html);
        let apple_music_url =
            self.extract_href_starting_with(&html, "https://www.shazam.com/applemusic/song/");
        let mut final_title = title.unwrap_or_else(|| "Unknown".to_owned());
        let mut final_artist = artist.unwrap_or_else(|| "Unknown".to_owned());
        if final_title == "Unknown"
            && let Some(og_title) = self.extract_meta_content(&html, "og:title")
        {
            if let Some(caps) = og_title_regex().captures(&og_title) {
                final_title = caps
                    .get(1)
                    .map(|m| m.as_str().to_owned())
                    .unwrap_or_else(|| og_title.clone());
                final_artist = caps
                    .get(2)
                    .map(|m| m.as_str().to_owned())
                    .unwrap_or_else(|| "Unknown".to_owned());
            } else {
                final_title = og_title;
            }
        }
        if final_title == "Unknown" && apple_music_url.is_none() {
            return LoadResult::Empty {};
        }
        let clean_url = url.strip_suffix('/').unwrap_or(url);
        let identifier = clean_url
            .split('/')
            .next_back()
            .unwrap_or("unknown")
            .to_owned();
        let mut track = Track::new(TrackInfo {
            identifier,
            is_seekable: true,
            author: final_artist,
            length: duration_ms,
            is_stream: false,
            position: 0,
            title: final_title,
            uri: Some(url.to_owned()),
            artwork_url: artwork_url.or_else(|| self.extract_meta_content(&html, "og:image")),
            isrc,
            source_name: "shazam".to_owned(),
        });
        track.plugin_info = serde_json::json!({
            "albumName": null,
            "albumUrl": null,
            "artistUrl": null,
            "artistArtworkUrl": null,
            "previewUrl": null,
            "isPreview": false
        });
        LoadResult::Track(track)
    }
    fn extract_text_after_class(&self, html: &str, class_part: &str) -> Option<String> {
        let mut from = 0;
        while let Some(c) = html[from..].find("class=\"") {
            let c = from + c;
            let q = html[c + 7..].find('"').map(|i| c + 7 + i)?;
            let cls = &html[c + 7..q];
            if cls.contains(class_part) {
                let gt = html[q..].find('>').map(|i| q + i)?;
                let lt = html[gt + 1..].find('<').map(|i| gt + 1 + i)?;
                let text = html[gt + 1..lt].trim().to_owned();
                if !text.is_empty() {
                    return Some(text);
                }
            }
            from = q + 1;
        }
        None
    }
    fn extract_href_starting_with(&self, html: &str, prefix: &str) -> Option<String> {
        let pattern = format!("href=\"{prefix}\"");
        if let Some(i) = html.find(&pattern) {
            let start = i + 6;
            if let Some(end) = html[start..].find('"') {
                return Some(html[start..start + end].to_owned());
            }
        }
        None
    }
    fn extract_artwork(&self, html: &str) -> Option<String> {
        if let Some(og) = self.extract_meta_content(html, "og:image") {
            return Some(og);
        }
        let mut alt_idx = html.find("alt=\"album cover\"");
        if alt_idx.is_none() {
            alt_idx = html.find("alt=\"song thumbnail\"");
        }
        let alt_idx = alt_idx?;
        let img_start = html[..alt_idx].rfind("<img")?;
        let img_end = html[alt_idx..].find('>')? + alt_idx;
        let tag = &html[img_start..img_end + 1];
        if let Some(s) = tag.find("srcset=\"") {
            let val_start = s + 8;
            if let Some(val_end) = tag[val_start..].find('"') {
                let srcset = &tag[val_start..val_start + val_end];
                let space = srcset.find(' ').unwrap_or(srcset.len());
                return Some(srcset[..space].to_owned());
            }
        }
        None
    }
    fn extract_isrc(&self, html: &str) -> Option<String> {
        let tokens = ["\"isrc\"", "\\\"isrc\\\""];
        for token in tokens {
            let mut from = 0;
            while let Some(at) = html[from..].find(token) {
                let at = from + at;
                from = at + token.len();
                let mut i = html[from..].find(':')? + from + 1;
                while i < html.len() {
                    let bytes = html.as_bytes();
                    if bytes[i] != b' '
                        && bytes[i] != b'\t'
                        && bytes[i] != b'\n'
                        && bytes[i] != b'\r'
                    {
                        break;
                    }
                    i += 1;
                }
                while i < html.len() && html.as_bytes()[i] == b'\\' {
                    i += 1;
                }
                if i >= html.len() || html.as_bytes()[i] != b'"' {
                    continue;
                }
                i += 1;
                if i + 12 > html.len() {
                    continue;
                }
                let isrc_cand = &html[i..i + 12];
                if self.is_valid_isrc(isrc_cand) {
                    return Some(isrc_cand.to_owned());
                }
            }
        }
        None
    }
    fn is_valid_isrc(&self, s: &str) -> bool {
        if s.len() != 12 {
            return false;
        }
        let b = s.as_bytes();
        // 2 upper
        if !b[0].is_ascii_uppercase() || !b[1].is_ascii_uppercase() {
            return false;
        }
        // 3 upper or digit
        for &c in &b[2..5] {
            if !c.is_ascii_uppercase() && !c.is_ascii_digit() {
                return false;
            }
        }
        // 7 digit
        for &c in &b[5..12] {
            if !c.is_ascii_digit() {
                return false;
            }
        }
        true
    }
    fn extract_duration(&self, html: &str) -> u64 {
        let needles = [
            "\"duration\":\"PT",
            "\"duration\": \"PT",
            "\\\"duration\\\":\\\"PT",
        ];
        for n in needles {
            if let Some(at) = html.find(n) {
                let start = at + n.len() - 2;
                let bracket = n.starts_with('\\');
                let end_char = if bracket { '\\' } else { '"' };
                if let Some(end) = html[start..].find(end_char) {
                    return self.parse_iso_duration(&html[start..start + end]);
                }
            }
        }
        0
    }
    fn parse_iso_duration(&self, iso: &str) -> u64 {
        if !iso.contains('T') {
            return 0;
        }
        let t_idx = iso.find('T').unwrap() + 1;
        let mut ms = 0.0;
        let mut current_num = String::new();
        for c in iso[t_idx..].chars() {
            if c.is_ascii_digit() || c == '.' {
                current_num.push(c);
            } else {
                let val = current_num.parse::<f64>().unwrap_or(0.0);
                match c {
                    'H' => ms += val * 3600000.0,
                    'M' => ms += val * 60000.0,
                    'S' => ms += val * 1000.0,
                    _ => break,
                }
                current_num.clear();
            }
        }
        ms as u64
    }
    fn extract_meta_content(&self, html: &str, prop: &str) -> Option<String> {
        let pattern = format!("<meta property=\"{prop}\" content=\"");
        if let Some(i) = html.find(&pattern) {
            let start = i + pattern.len();
            if let Some(end) = html[start..].find('"') {
                return Some(html[start..start + end].to_owned());
            }
        }
        None
    }
    pub fn base_request(&self, builder: reqwest::RequestBuilder) -> reqwest::RequestBuilder {
        builder.header(USER_AGENT, "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36")
    }
}
#[async_trait]
impl SourcePlugin for ShazamSource {
    fn name(&self) -> &str {
        "shazam"
    }
    fn can_handle(&self, identifier: &str) -> bool {
        self.search_prefixes()
            .iter()
            .any(|p| identifier.starts_with(p))
            || url_regex().is_match(identifier)
    }
    fn search_prefixes(&self) -> Vec<&str> {
        vec!["shsearch:", "szsearch:"]
    }
    fn is_mirror(&self) -> bool {
        true
    }
    async fn load(
        &self,
        identifier: &str,
        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
    ) -> LoadResult {
        if let Some(prefix) = self
            .search_prefixes()
            .into_iter()
            .find(|p| identifier.starts_with(p))
        {
            let query = &identifier[prefix.len()..];
            return self.search(query).await;
        }
        if url_regex().is_match(identifier) {
            return self.resolve_url(identifier).await;
        }
        LoadResult::Empty {}
    }
    async fn get_track(
        &self,
        _identifier: &str,
        _routeplanner: Option<Arc<dyn crate::routeplanner::RoutePlanner>>,
    ) -> Option<BoxedTrack> {
        None
    }
}
