//! Fetching and parsing of YouTube's public per-channel Atom feed.
//!
//! `https://www.youtube.com/feeds/videos.xml?channel_id=<id>` is public and
//! unauthenticated. It returns roughly the 15 most recent videos per channel
//! with no pagination and no backfill — that ceiling is inherent to the source.

use crate::models::Video;
use quick_xml::events::Event;
use quick_xml::Reader;

pub fn feed_url(channel_id: &str) -> String {
    format!("https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}")
}

/// Local names we capture text for, tracked as a small state machine while walking events.
#[derive(PartialEq, Clone, Copy)]
enum Field {
    None,
    VideoId,
    ChannelId,
    Title,
    Published,
    Description,
}

fn attr(e: &quick_xml::events::BytesStart, key: &str) -> Option<String> {
    e.attributes()
        .flatten()
        .find(|a| a.key.local_name().as_ref() == key)
        .map(|a| a.value.into_owned())
}

/// RFC 3339 timestamp -> Unix seconds. Returns 0 if unparseable so a single
/// bad entry sorts to the bottom rather than failing the whole feed.
fn to_unix(s: &str) -> i64 {
    chrono::DateTime::parse_from_rfc3339(s.trim())
        .map(|d| d.timestamp())
        .unwrap_or(0)
}

pub fn parse_atom(xml: &str) -> Result<Vec<Video>, String> {
    let mut reader = Reader::from_str(xml);
    reader.config_mut().trim_text(true);

    let mut videos = Vec::new();
    let mut in_entry = false;
    let mut field = Field::None;
    // media:group repeats <title> and <description>; keep the first (Atom) title
    // and don't let the later media one overwrite it.
    let mut cur: Option<Video> = None;
    let mut saw_title = false;

    loop {
        match reader.read_event() {
            Err(e) => return Err(format!("Malformed feed XML: {e}")),
            // An unclosed <entry> at EOF means the response was truncated. quick-xml
            // does not treat that as an error, but we must: silently storing half a
            // channel's videos is worse than failing the refresh for that channel.
            Ok(Event::Eof) => {
                if in_entry {
                    return Err("Feed XML ended inside an <entry> (truncated response).".into());
                }
                break;
            }

            Ok(Event::Start(e)) => {
                let name = e.name().local_name();
                match name.as_ref() {
                    "entry" => {
                        in_entry = true;
                        saw_title = false;
                        cur = Some(Video {
                            id: String::new(),
                            channel_id: String::new(),
                            title: String::new(),
                            description: String::new(),
                            published: 0,
                            thumb_url: String::new(),
                            views: 0,
                            is_short: false,
                        });
                    }
                    "videoId" if in_entry => field = Field::VideoId,
                    "channelId" if in_entry => field = Field::ChannelId,
                    "title" if in_entry && !saw_title => field = Field::Title,
                    "published" if in_entry => field = Field::Published,
                    "description" if in_entry => field = Field::Description,
                    _ => {}
                }
            }

            Ok(Event::Empty(e)) => {
                if !in_entry {
                    continue;
                }
                let name = e.name().local_name();
                if let Some(v) = cur.as_mut() {
                    match name.as_ref() {
                        "link" => {
                            if attr(&e, "rel").as_deref() == Some("alternate") {
                                if let Some(href) = attr(&e, "href") {
                                    v.is_short = href.contains("/shorts/");
                                }
                            }
                        }
                        "thumbnail" => {
                            if let Some(u) = attr(&e, "url") {
                                if v.thumb_url.is_empty() {
                                    v.thumb_url = u;
                                }
                            }
                        }
                        "statistics" => {
                            if let Some(views) = attr(&e, "views") {
                                v.views = views.parse().unwrap_or(0);
                            }
                        }
                        _ => {}
                    }
                }
            }

            Ok(Event::Text(t)) => {
                if !in_entry || field == Field::None {
                    continue;
                }
                let text = t.xml10_content();
                if let Some(v) = cur.as_mut() {
                    match field {
                        Field::VideoId => v.id.push_str(&text),
                        Field::ChannelId => v.channel_id.push_str(&text),
                        Field::Title => v.title.push_str(&text),
                        Field::Published => v.published = to_unix(&text),
                        Field::Description => v.description.push_str(&text),
                        Field::None => {}
                    }
                }
            }

            Ok(Event::End(e)) => {
                let name = e.name().local_name();
                if name.as_ref() == "title" && field == Field::Title {
                    saw_title = true;
                }
                if name.as_ref() == "entry" {
                    in_entry = false;
                    if let Some(v) = cur.take() {
                        if !v.id.is_empty() {
                            videos.push(v);
                        }
                    }
                }
                field = Field::None;
            }

            _ => {}
        }
    }

    Ok(videos)
}

pub async fn fetch_channel(
    client: &reqwest::Client,
    channel_id: &str,
) -> Result<Vec<Video>, String> {
    let resp = client
        .get(feed_url(channel_id))
        .send()
        .await
        .map_err(|e| format!("Request failed: {e}"))?;

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

    let body = resp
        .text()
        .await
        .map_err(|e| format!("Could not read feed body: {e}"))?;

    let mut videos = parse_atom(&body)?;
    // Trust the requested id over the payload for channels that return an empty one.
    for v in &mut videos {
        if v.channel_id.is_empty() {
            v.channel_id = channel_id.to_string();
        }
    }
    Ok(videos)
}

#[cfg(test)]
mod tests {
    use super::*;
    const FIXTURE: &str = include_str!("fixtures/ltt_feed.xml");

    #[test]
    fn parses_all_entries_from_real_feed() {
        let v = parse_atom(FIXTURE).unwrap();
        assert_eq!(v.len(), 15);
    }

    #[test]
    fn extracts_core_fields() {
        let v = parse_atom(FIXTURE).unwrap();
        let first = &v[0];
        assert_eq!(first.id, "tklAv8hcG9s");
        assert_eq!(first.channel_id, "UCXuqSBlHAE6Xw-yeJA0Tunw");
        assert!(first.title.contains("Linus"), "got title: {}", first.title);
        assert!(first.published > 1_700_000_000);
        assert!(first.thumb_url.contains("tklAv8hcG9s"));
        assert!(first.views > 0);
        assert!(!first.description.is_empty());
    }

    #[test]
    fn title_is_the_atom_title_not_the_media_group_one() {
        let v = parse_atom(FIXTURE).unwrap();
        assert_eq!(
            v[0].title,
            "Former LTT employee Shares a Memorable Linus Story"
        );
    }

    #[test]
    fn feed_entries_are_all_from_one_channel() {
        let v = parse_atom(FIXTURE).unwrap();
        assert!(v.iter().all(|x| x.channel_id == "UCXuqSBlHAE6Xw-yeJA0Tunw"));
    }

    #[test]
    fn detects_shorts() {
        let v = parse_atom(FIXTURE).unwrap();
        assert!(v[0].is_short, "first fixture entry is a /shorts/ link");
        assert!(
            v.iter().any(|x| !x.is_short),
            "fixture should contain regular videos too"
        );
    }

    #[test]
    fn tolerates_missing_optional_fields() {
        let xml = r#"<feed xmlns="http://www.w3.org/2005/Atom"
          xmlns:yt="http://www.youtube.com/xml/schemas/2015"
          xmlns:media="http://search.yahoo.com/mrss/">
          <entry><yt:videoId>abc</yt:videoId><yt:channelId>UCq</yt:channelId>
            <title>Bare</title>
            <link rel="alternate" href="https://www.youtube.com/watch?v=abc"/>
            <published>2026-01-01T00:00:00+00:00</published>
          </entry></feed>"#;
        let v = parse_atom(xml).unwrap();
        assert_eq!(v.len(), 1);
        assert_eq!(v[0].views, 0);
        assert_eq!(v[0].description, "");
        assert_eq!(v[0].thumb_url, "");
        assert!(!v[0].is_short);
        assert_eq!(v[0].published, 1_767_225_600);
    }

    #[test]
    fn errors_on_malformed_xml() {
        assert!(parse_atom("<feed><entry>").is_err());
    }

    #[test]
    fn returns_empty_for_feed_with_no_entries() {
        let xml = r#"<feed xmlns="http://www.w3.org/2005/Atom"><title>Empty</title></feed>"#;
        assert!(parse_atom(xml).unwrap().is_empty());
    }

    #[test]
    fn builds_expected_feed_url() {
        assert_eq!(
            feed_url("UCq"),
            "https://www.youtube.com/feeds/videos.xml?channel_id=UCq"
        );
    }
}
