//! Beatport Top 100 chart scraping.
//!
//! Fetches a Beatport genre page and extracts track data from embedded JSON
//! (__NEXT_DATA__ or inline script arrays).  Tracks are returned as simple
//! structs suitable for display and Spotify search matching.

use regex::Regex;
use serde::{Deserialize, Serialize};

/// A single Beatport chart entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BeatportTrack {
    pub rank: u32,
    pub name: String,
    pub artists: String,
    #[serde(default)]
    pub label: String,
}

/// All supported Beatport genre slugs (name → slug).
pub fn genre_list() -> Vec<(&'static str, &'static str)> {
    vec![
        ("Overall", ""),
        ("140 / Deep Dubstep / Grime", "140-deep-dubstep-grime/95"),
        ("African", "african/102"),
        ("Afro House", "afro-house/89"),
        ("Amapiano", "amapiano/98"),
        ("Ambient / Experimental", "ambient-experimental/100"),
        ("Bass / Club", "bass-club/85"),
        ("Bass House", "bass-house/91"),
        ("Brazilian Funk", "brazilian-funk/101"),
        ("Breaks / Breakbeat / UK Bass", "breaks-breakbeat-uk-bass/9"),
        ("Caribbean", "caribbean/103"),
        ("Country", "country/104"),
        ("Dance / Pop", "dance-pop/39"),
        ("Deep House", "deep-house/12"),
        ("DJ Tools", "dj-tools/16"),
        ("Downtempo", "downtempo/63"),
        ("Drum & Bass", "drum-bass/1"),
        ("Dubstep", "dubstep/18"),
        ("Electro (Classic / Detroit / Modern)", "electro-classic-detroit-modern/94"),
        ("Electronica", "electronica/3"),
        ("Funky House", "funky-house/81"),
        ("Hard Dance / Hardcore / Neo Rave", "hard-dance-hardcore-neo-rave/8"),
        ("Hard Techno", "hard-techno/2"),
        ("Hip-Hop", "hip-hop/105"),
        ("House", "house/5"),
        ("Indie Dance", "indie-dance/37"),
        ("Jackin House", "jackin-house/97"),
        ("Latin", "latin/106"),
        ("Latin Electronic", "latin-electronic/111"),
        ("Mainstage", "mainstage/96"),
        ("Melodic House & Techno", "melodic-house-techno/90"),
        ("Minimal / Deep Tech", "minimal-deep-tech/14"),
        ("Nu Disco / Disco", "nu-disco-disco/50"),
        ("Organic House", "organic-house/93"),
        ("Pop", "pop/107"),
        ("Progressive House", "progressive-house/15"),
        ("Psy-Trance", "psy-trance/13"),
        ("R&B", "rb/108"),
        ("Rock", "rock/109"),
        ("Tech House", "tech-house/11"),
        ("Techno (Peak Time / Driving)", "techno-peak-time-driving/6"),
        ("Techno (Raw / Deep / Hypnotic)", "techno-raw-deep-hypnotic/92"),
        ("Trance (Main Floor)", "trance-main-floor/7"),
        ("Trance (Raw / Deep / Hypnotic)", "trance-raw-deep-hypnotic/99"),
        ("Trap / Future Bass", "trap-future-bass/38"),
        ("UK Garage / Bassline", "uk-garage-bassline/86"),
    ]
}

/// Fetch and parse the Beatport Top 100 for a given genre slug.
pub async fn fetch_top_100(
    http: &reqwest::Client,
    genre_slug: &str,
) -> Result<Vec<BeatportTrack>, String> {
    let url = if genre_slug.is_empty() {
        "https://www.beatport.com/top-100".to_string()
    } else {
        format!("https://www.beatport.com/genre/{genre_slug}/top-100")
    };

    let resp = http
        .get(&url)
        .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36")
        .header("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
        .header("Accept-Language", "en-US,en;q=0.5")
        .send()
        .await
        .map_err(|e| format!("Beatport fetch error: {e}"))?;

    if !resp.status().is_success() {
        return Err(format!("Beatport returned HTTP {}", resp.status()));
    }

    let html = resp.text().await.map_err(|e| format!("Beatport read error: {e}"))?;
    parse_beatport_html(&html)
}

/// Parse Beatport HTML for track data using multiple strategies.
fn parse_beatport_html(html: &str) -> Result<Vec<BeatportTrack>, String> {
    // Strategy 1: __NEXT_DATA__ JSON blob
    let re_next = Regex::new(r#"<script id="__NEXT_DATA__"[^>]*>(.*?)</script>"#).unwrap();
    if let Some(cap) = re_next.captures(html) {
        if let Ok(data) = serde_json::from_str::<serde_json::Value>(&cap[1]) {
            let tracks = extract_from_next_data(&data);
            if !tracks.is_empty() {
                return Ok(tracks);
            }
        }
    }

    // Strategy 2: Find JSON arrays with track-like data in script tags
    for pattern in &[
        r#""tracks"\s*:\s*(\[[\s\S]*?\])\s*[,}]"#,
        r#""results"\s*:\s*(\[[\s\S]*?\])\s*[,}]"#,
    ] {
        let re = Regex::new(pattern).unwrap();
        if let Some(cap) = re.captures(html) {
            if let Ok(arr) = serde_json::from_str::<Vec<serde_json::Value>>(&cap[1]) {
                let tracks = extract_track_array(&arr);
                if !tracks.is_empty() {
                    return Ok(tracks);
                }
            }
        }
    }

    // Strategy 3: ld+json structured data
    let re_ld = Regex::new(r#"<script type="application/ld\+json">(.*?)</script>"#).unwrap();
    for cap in re_ld.captures_iter(html) {
        if let Ok(ld) = serde_json::from_str::<serde_json::Value>(&cap[1]) {
            let items = if ld.is_array() {
                ld.as_array().cloned().unwrap_or_default()
            } else {
                ld["itemListElement"]
                    .as_array()
                    .cloned()
                    .unwrap_or_default()
            };
            let mut tracks = Vec::new();
            for (i, it) in items.iter().enumerate() {
                let obj = it.get("item").unwrap_or(it);
                let t = obj.get("track").unwrap_or(obj);
                if let Some(name) = t["name"].as_str() {
                    let artist = if let Some(by) = t.get("byArtist") {
                        by["name"].as_str().unwrap_or("").to_string()
                    } else {
                        String::new()
                    };
                    tracks.push(BeatportTrack {
                        rank: (i + 1) as u32,
                        name: name.to_string(),
                        artists: artist,
                        label: String::new(),
                    });
                }
            }
            if tracks.len() >= 5 {
                return Ok(truncate(tracks));
            }
        }
    }

    Err("No tracks found - Beatport may have changed their page structure.".into())
}

/// Extract tracks from __NEXT_DATA__ JSON.
fn extract_from_next_data(data: &serde_json::Value) -> Vec<BeatportTrack> {
    let pp = &data["props"]["pageProps"];

    // Primary: dehydratedState -> queries -> state -> data -> results
    if let Some(queries) = pp["dehydratedState"]["queries"].as_array() {
        for q in queries {
            if let Some(arr) = q["state"]["data"]["results"].as_array() {
                let tracks = extract_track_array(arr);
                if !tracks.is_empty() {
                    return tracks;
                }
            }
        }
    }

    // Try direct keys
    for key in &["tracks", "results", "chart", "topTracks"] {
        if let Some(arr) = pp[key].as_array() {
            let tracks = extract_track_array(arr);
            if !tracks.is_empty() {
                return tracks;
            }
        }
    }

    // Fallback: deep search
    deep_search(data, 0)
}

/// Recursively search JSON for an array of 10+ track-like objects.
fn deep_search(obj: &serde_json::Value, depth: usize) -> Vec<BeatportTrack> {
    if depth > 8 {
        return vec![];
    }
    if let Some(arr) = obj.as_array() {
        if arr.len() >= 10 {
            let tracks = extract_track_array(arr);
            if !tracks.is_empty() {
                return tracks;
            }
        }
    }
    if let Some(map) = obj.as_object() {
        for v in map.values() {
            let r = deep_search(v, depth + 1);
            if !r.is_empty() {
                return r;
            }
        }
    }
    vec![]
}

/// Convert a JSON array of track-like objects into BeatportTrack structs.
fn extract_track_array(arr: &[serde_json::Value]) -> Vec<BeatportTrack> {
    let mut tracks = Vec::new();
    for (i, item) in arr.iter().enumerate() {
        let obj = match item.as_object() {
            Some(o) => o,
            None => continue,
        };

        let name = obj
            .get("name")
            .or_else(|| obj.get("title"))
            .or_else(|| obj.get("track_name"))
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
        if name.is_empty() {
            continue;
        }

        let artists = match obj.get("artists").or_else(|| obj.get("artist")) {
            Some(serde_json::Value::Array(arr)) => arr
                .iter()
                .filter_map(|a| {
                    if let Some(obj) = a.as_object() {
                        obj.get("name").and_then(|n| n.as_str()).map(|s| s.to_string())
                    } else {
                        a.as_str().map(|s| s.to_string())
                    }
                })
                .collect::<Vec<_>>()
                .join(", "),
            Some(serde_json::Value::Object(o)) => {
                o.get("name").and_then(|n| n.as_str()).unwrap_or("").to_string()
            }
            Some(serde_json::Value::String(s)) => s.clone(),
            _ => String::new(),
        };

        // Extract label from release.label
        let label = obj
            .get("release")
            .and_then(|r| r.get("label"))
            .or_else(|| obj.get("label"))
            .and_then(|l| {
                if let Some(o) = l.as_object() {
                    o.get("name").and_then(|n| n.as_str()).map(|s| s.to_string())
                } else {
                    l.as_str().map(|s| s.to_string())
                }
            })
            .unwrap_or_default();

        tracks.push(BeatportTrack {
            rank: (i + 1) as u32,
            name,
            artists,
            label,
        });
    }

    if tracks.len() >= 5 {
        truncate(tracks)
    } else {
        vec![]
    }
}

/// Strip Beatport mix suffixes that hurt Spotify search matching.
pub fn clean_bp_name(name: &str) -> String {
    let re = Regex::new(
        r"(?i)\s*\((Extended|Original|Club|Radio|Dub|Instrumental|VIP|Short)(\s+(Mix|Edit|Remix|Version|Dub))?\)"
    ).unwrap();
    let cleaned = re.replace_all(name, "").trim().to_string();
    if cleaned.is_empty() {
        name.to_string()
    } else {
        cleaned
    }
}

fn truncate(mut tracks: Vec<BeatportTrack>) -> Vec<BeatportTrack> {
    tracks.truncate(100);
    tracks
}
