use chrono::{DateTime, NaiveDate};
use scraper::{Html, Selector};
use serde_json::Value;
use url::Url;

use crate::error::{EbookmError, Result};
use crate::source::{LoadedSource, SourceOrigin};

#[derive(Debug, Clone)]
pub struct ExtractedArticle {
    pub title: String,
    pub author: Option<String>,
    pub published: Option<NaiveDate>,
    pub canonical_url: Option<Url>,
    pub body_html: String,
}

#[derive(Debug, Clone, serde::Serialize)]
pub struct InspectResult {
    pub title: Option<String>,
    pub author: Option<String>,
    pub published: Option<String>,
    pub canonical_url: Option<String>,
}

pub fn extract_article(loaded: &LoadedSource) -> Result<ExtractedArticle> {
    let document = Html::parse_document(&loaded.html);
    let json_ld = extract_primary_json_ld(&document);
    let title = extract_title(&document, &json_ld).ok_or_else(|| EbookmError::Extraction {
        input: origin_label(&loaded.origin),
    })?;

    let author = select_content(
        &document,
        &[
            r#"meta[name="author"]"#,
            r#"meta[property="article:author"]"#,
        ],
        "content",
    )
    .or_else(|| {
        select_text(
            &document,
            &[
                "[data-testid='author-name']",
                ".byline",
                ".byline-wrapper a",
                "address",
            ],
        )
    })
    .or_else(|| json_ld_author(&json_ld));

    let published = select_content(
        &document,
        &[r#"meta[property="article:published_time"]"#, "time"],
        "content",
    )
    .or_else(|| select_attr(&document, &["time"], "datetime"))
    .or_else(|| json_ld_string(&json_ld, "datePublished"))
    .and_then(parse_date);

    let canonical_url = select_attr(&document, &[r#"link[rel="canonical"]"#], "href")
        .or_else(|| match &loaded.origin {
            SourceOrigin::Remote(url) => Some(url.to_string()),
            SourceOrigin::LocalFile(_) => None,
        })
        .and_then(|raw| Url::parse(&raw).ok());

    let body_html = select_html(
        &document,
        &[
            ".available-content .body.markup",
            ".available-content .markup",
            "article .body.markup",
            ".newsletter-post .body.markup",
            "article",
            "main",
            "body",
        ],
    )
    .ok_or_else(|| EbookmError::Extraction {
        input: origin_label(&loaded.origin),
    })?;

    Ok(ExtractedArticle {
        title,
        author,
        published,
        canonical_url,
        body_html,
    })
}

pub fn inspect_article(loaded: &LoadedSource) -> Result<InspectResult> {
    let extracted = extract_article(loaded)?;
    Ok(InspectResult {
        title: Some(extracted.title),
        author: extracted.author,
        published: extracted.published.map(|date| date.to_string()),
        canonical_url: extracted.canonical_url.map(|url| url.to_string()),
    })
}

fn select_content(document: &Html, selectors: &[&str], attr: &str) -> Option<String> {
    selectors.iter().find_map(|selector| {
        let selector = Selector::parse(selector).ok()?;
        document
            .select(&selector)
            .next()
            .and_then(|node| node.value().attr(attr))
            .map(clean_text)
    })
}

fn extract_title(document: &Html, json_ld: &Option<Value>) -> Option<String> {
    select_text(
        document,
        &[
            "h1.post-title",
            ".post-title",
            r#"article .post-title"#,
            "title",
            "h1",
        ],
    )
    .or_else(|| json_ld_string(json_ld, "headline"))
}

fn select_text(document: &Html, selectors: &[&str]) -> Option<String> {
    selectors.iter().find_map(|selector| {
        let selector = Selector::parse(selector).ok()?;
        document
            .select(&selector)
            .next()
            .map(|node| clean_text(&node.text().collect::<String>()))
    })
}

fn select_attr(document: &Html, selectors: &[&str], attr: &str) -> Option<String> {
    selectors.iter().find_map(|selector| {
        let selector = Selector::parse(selector).ok()?;
        document
            .select(&selector)
            .next()
            .and_then(|node| node.value().attr(attr))
            .map(clean_text)
    })
}

fn select_html(document: &Html, selectors: &[&str]) -> Option<String> {
    selectors.iter().find_map(|selector| {
        let selector = Selector::parse(selector).ok()?;
        document
            .select(&selector)
            .next()
            .map(|node| node.inner_html())
    })
}

fn clean_text(value: &str) -> String {
    value.split_whitespace().collect::<Vec<_>>().join(" ")
}

fn parse_date(value: String) -> Option<NaiveDate> {
    DateTime::parse_from_rfc3339(&value)
        .map(|parsed| parsed.date_naive())
        .ok()
        .or_else(|| NaiveDate::parse_from_str(&value, "%Y-%m-%d").ok())
        .or_else(|| NaiveDate::parse_from_str(&value, "%b %d, %Y").ok())
}

fn origin_label(origin: &SourceOrigin) -> String {
    match origin {
        SourceOrigin::Remote(url) => url.to_string(),
        SourceOrigin::LocalFile(path) => path.display().to_string(),
    }
}

fn extract_primary_json_ld(document: &Html) -> Option<Value> {
    let selector = Selector::parse(r#"script[type="application/ld+json"]"#).ok()?;
    for node in document.select(&selector) {
        let raw = node.inner_html();
        let Ok(value) = serde_json::from_str::<Value>(&raw) else {
            continue;
        };
        if value.get("@type").and_then(Value::as_str).is_some() {
            return Some(value);
        }
    }
    None
}

fn json_ld_string(json_ld: &Option<Value>, key: &str) -> Option<String> {
    json_ld
        .as_ref()?
        .get(key)?
        .as_str()
        .map(|value| value.to_string())
}

fn json_ld_author(json_ld: &Option<Value>) -> Option<String> {
    let author = json_ld.as_ref()?.get("author")?;
    if let Some(author_name) = author
        .get(0)
        .and_then(|entry| entry.get("name"))
        .and_then(Value::as_str)
    {
        return Some(author_name.to_string());
    }
    if let Some(author_name) = author.get("name").and_then(Value::as_str) {
        return Some(author_name.to_string());
    }
    None
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::source::LoadedSource;

    #[test]
    fn extracts_substack_article_body_without_page_chrome() {
        let html = r#"<!doctype html>
<html>
  <head>
    <meta property="og:title" content="A Contested Island" />
    <meta name="author" content="John Gu" />
    <link rel="canonical" href="https://ageofpeace.substack.com/p/a-contested-island" />
    <script type="application/ld+json">{"@context":"https://schema.org","@type":"NewsArticle","headline":"A Contested Island","datePublished":"2026-03-03T23:37:00+00:00","author":[{"@type":"Person","name":"John Gu"}]}</script>
  </head>
  <body>
    <article class="typography newsletter-post post">
      <div class="post-header">
        <h1 class="post-title">Chapter 1: A Contested Island</h1>
      </div>
      <div class="available-content">
        <div class="body markup">
          <p>First paragraph.</p>
          <p>Second paragraph.</p>
        </div>
      </div>
      <div class="post-footer">
        <button>Share</button>
      </div>
    </article>
  </body>
</html>"#;

        let loaded = LoadedSource {
            origin: SourceOrigin::Remote(
                Url::parse("https://ageofpeace.substack.com/p/a-contested-island").expect("url"),
            ),
            html: html.to_string(),
        };

        let extracted = extract_article(&loaded).expect("extract article");
        assert_eq!(extracted.title, "Chapter 1: A Contested Island");
        assert_eq!(extracted.author.as_deref(), Some("John Gu"));
        assert_eq!(
            extracted.published,
            Some(NaiveDate::from_ymd_opt(2026, 3, 3).expect("date"))
        );
        assert!(extracted.body_html.contains("First paragraph."));
        assert!(!extracted.body_html.contains("post-header"));
        assert!(!extracted.body_html.contains("Share"));
    }
}
