//! Start-page tutorial-video list, sourced live from the official YouTube
//! playlist at boot — nothing is baked into the binary or the installer.
//!
//! The playlist page is scanned for its ordered video ids, each id's title
//! comes from the stable key-less oEmbed endpoint, and each thumbnail is
//! downloaded once and cached under the user config directory
//! (`video_thumbs/<id>.jpg`), so later launches show the list instantly and
//! offline launches still show everything fetched before.

/// The official tutorials playlist.
pub const PLAYLIST_URL: &str =
    "https://youtube.com/playlist?list=PLZq_TEkIFh9bAnoOX1HiCAunm3anZDBOl";

const PLAYLIST_ID: &str = "PLZq_TEkIFh9bAnoOX1HiCAunm3anZDBOl";

/// One playlist entry, ready for the Start page.
#[derive(Clone, Debug)]
pub struct VideoEntry {
    pub id: String,
    pub title: String,
    /// JPEG bytes of the thumbnail (from cache or the network), if available.
    pub thumb: Option<Vec<u8>>,
}

/// Watch URL for one of the playlist's videos, keeping the playlist context.
pub fn watch_url(id: &str) -> String {
    format!("https://www.youtube.com/watch?v={id}&list={PLAYLIST_ID}")
}

#[cfg(not(target_arch = "wasm32"))]
const UA: &str = concat!("OpenCADStudio/", env!("CARGO_PKG_VERSION"));

/// Directory of the on-disk thumbnail / listing cache.
#[cfg(not(target_arch = "wasm32"))]
fn cache_dir() -> Option<std::path::PathBuf> {
    crate::config::config_dir().map(|d| d.join("video_thumbs"))
}

/// Load the listing cached by the previous successful fetch — the instant,
/// offline-friendly Start-page state while the live fetch runs.
#[cfg(not(target_arch = "wasm32"))]
pub fn load_cached() -> Vec<VideoEntry> {
    let Some(dir) = cache_dir() else {
        return Vec::new();
    };
    let Ok(listing) = std::fs::read_to_string(dir.join("playlist.txt")) else {
        return Vec::new();
    };
    listing
        .lines()
        .filter_map(|line| {
            let (id, title) = line.split_once('\t')?;
            if id.len() != 11 || title.is_empty() {
                return None;
            }
            Some(VideoEntry {
                id: id.to_string(),
                title: title.to_string(),
                thumb: std::fs::read(dir.join(format!("{id}.jpg"))).ok(),
            })
        })
        .collect()
}

/// Fetch the playlist's current entries, in playlist order, refreshing the
/// on-disk cache. `Err` when offline or the page markup changes — the caller
/// keeps whatever [`load_cached`] provided.
#[cfg(not(target_arch = "wasm32"))]
pub fn fetch_playlist() -> Result<Vec<VideoEntry>, String> {
    let agent: ureq::Agent = ureq::Agent::config_builder()
        .timeout_global(Some(std::time::Duration::from_secs(15)))
        .build()
        .into();
    let page = agent
        .get(PLAYLIST_URL)
        .header("User-Agent", "Mozilla/5.0")
        .call()
        .map_err(|e| e.to_string())?
        .body_mut()
        .with_config()
        .limit(8 * 1024 * 1024)
        .read_to_string()
        .map_err(|e| e.to_string())?;

    // Ordered, de-duplicated video ids from the page's embedded JSON. The
    // `"videoId":"…"` pattern has been stable for years and doubles as the
    // canary: no matches → markup changed → keep the cached list.
    let mut ids: Vec<String> = Vec::new();
    let mut rest = page.as_str();
    while let Some(pos) = rest.find("\"videoId\":\"") {
        let start = pos + "\"videoId\":\"".len();
        let Some(end) = rest[start..].find('"') else {
            break;
        };
        let id = &rest[start..start + end];
        if id.len() == 11 && !ids.iter().any(|k| k == id) {
            ids.push(id.to_string());
        }
        rest = &rest[start + end..];
        if ids.len() >= 50 {
            break;
        }
    }
    if ids.is_empty() {
        return Err("no videos found in playlist page".into());
    }

    let dir = cache_dir();
    if let Some(d) = &dir {
        let _ = std::fs::create_dir_all(d);
    }

    let mut out: Vec<VideoEntry> = Vec::with_capacity(ids.len());
    for id in ids {
        // Title via oEmbed — stable and key-less. A video without a title
        // (deleted / private) is skipped.
        let title = agent
            .get(&format!(
                "https://www.youtube.com/oembed?url=https://youtu.be/{id}&format=json"
            ))
            .header("User-Agent", UA)
            .call()
            .ok()
            .and_then(|mut r| r.body_mut().read_to_string().ok())
            .and_then(|body| {
                serde_json::from_str::<serde_json::Value>(&body)
                    .ok()?
                    .get("title")?
                    .as_str()
                    .map(str::to_string)
            });
        let Some(title) = title else { continue };

        // Thumbnail: cache first, else download once and store it.
        let cache_path = dir.as_ref().map(|d| d.join(format!("{id}.jpg")));
        let mut thumb = cache_path.as_ref().and_then(|p| std::fs::read(p).ok());
        if thumb.is_none() {
            let fetched = agent
                .get(&format!("https://i.ytimg.com/vi/{id}/mqdefault.jpg"))
                .header("User-Agent", UA)
                .call()
                .ok()
                .and_then(|mut r| {
                    r.body_mut()
                        .with_config()
                        .limit(2 * 1024 * 1024)
                        .read_to_vec()
                        .ok()
                });
            if let (Some(bytes), Some(p)) = (&fetched, &cache_path) {
                let _ = std::fs::write(p, bytes);
            }
            thumb = fetched;
        }
        out.push(VideoEntry { id, title, thumb });
    }
    if out.is_empty() {
        return Err("no video titles resolved".into());
    }

    // The playlist page lists oldest-first; the Start page wants the newest
    // tutorials on top. Reversed before persisting, so the cached listing
    // (and every later offline launch) shares the same order.
    out.reverse();

    // Persist the listing so the next launch starts from it.
    if let Some(d) = &dir {
        let listing: String = out
            .iter()
            .map(|v| format!("{}\t{}\n", v.id, v.title.replace(['\t', '\n'], " ")))
            .collect();
        let _ = std::fs::write(d.join("playlist.txt"), listing);
    }
    Ok(out)
}
