//! Parse PwC Albania publications listing HTML and normalize document URLs.

use anyhow::{anyhow, Result};
use percent_encoding::percent_decode_str;
use scraper::{Html, Selector};
use std::collections::HashMap;
use url::Url;

pub const PWC_AL_PUBLICATIONS_URL: &str = "https://www.pwc.com/al/en/Publications.html";

const BASE: &str = "https://www.pwc.com";

/// Resolved link from the publications page (title + absolute URL).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublicationLink {
    pub title: String,
    pub url: String,
}

/// Allowed host for document fetches (SSRF guard).
pub fn assert_pwc_host(url_str: &str) -> Result<Url> {
    let u = Url::parse(url_str).map_err(|e| anyhow!("invalid URL: {}", e))?;
    match u.scheme() {
        "http" | "https" => {}
        other => return Err(anyhow!("scheme not allowed: {}", other)),
    }
    let host = u.host_str().ok_or_else(|| anyhow!("URL missing host"))?;
    if host != "www.pwc.com" {
        return Err(anyhow!("host not allowed: {}", host));
    }
    Ok(u)
}

/// Join relative or absolute href against PwC base and validate host.
pub fn normalize_publication_href(href: &str) -> Result<String> {
    let href = href.trim();
    if href.is_empty() || href.starts_with('#') {
        return Err(anyhow!("empty or fragment-only href"));
    }
    let base = Url::parse(BASE).expect("valid base");
    let joined = base.join(href).map_err(|e| anyhow!("join href: {}", e))?;
    assert_pwc_host(joined.as_str())?;
    let mut u = joined;
    u.set_fragment(None);
    Ok(u.into())
}

/// Extract title from `.strip-btn__text` if present, else flatten anchor text.
fn title_from_anchor(html: scraper::element_ref::ElementRef<'_>) -> String {
    let sel_text =
        Selector::parse(".strip-btn__text").expect("selector");
    if let Some(span) = html.select(&sel_text).next() {
        let t = span.text().collect::<Vec<_>>().join(" ");
        let t = t.trim();
        if !t.is_empty() {
            return t.to_string();
        }
    }
    html.text().collect::<Vec<_>>().join(" ").trim().to_string()
}

/// Parse listing page HTML; returns deduplicated links (first title wins per canonical URL).
pub fn parse_publication_links(html: &str) -> Vec<PublicationLink> {
    let sel_listing = Selector::parse("div.secnav-twocol__listing a.strip-btn")
        .expect("selector");
    let document = Html::parse_document(html);
    let mut by_url: HashMap<String, PublicationLink> = HashMap::new();

    for a in document.select(&sel_listing) {
        let Some(href) = a.value().attr("href") else {
            continue;
        };
        let Ok(url) = normalize_publication_href(href) else {
            continue;
        };
        let title = title_from_anchor(a);
        by_url.entry(url.clone()).or_insert(PublicationLink { title, url });
    }

    let mut v: Vec<PublicationLink> = by_url.into_values().collect();
    v.sort_by(|a, b| a.url.cmp(&b.url));
    v
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssetKind {
    Pdf,
    Html,
}

pub fn classify_asset(path: &str) -> Option<AssetKind> {
    let lower = path.to_lowercase();
    if lower.ends_with(".pdf") {
        Some(AssetKind::Pdf)
    } else if lower.ends_with(".html") || lower.ends_with(".htm") {
        Some(AssetKind::Html)
    } else {
        None
    }
}

/// Derive S3 filename from URL path (last segment, percent-decoded); ASCII-safe fallback.
pub fn s3_filename_from_url(url: &str) -> Result<String> {
    let u = Url::parse(url).map_err(|e| anyhow!("parse: {}", e))?;
    let seg = u
        .path_segments()
        .and_then(|mut s| s.next_back())
        .filter(|s| !s.is_empty())
        .ok_or_else(|| anyhow!("no path segment"))?;
    let decoded = percent_decode_str(seg).decode_utf8_lossy().to_string();
    let decoded = decoded.trim();
    if decoded.is_empty() {
        return Err(anyhow!("empty filename"));
    }
    // Avoid path traversal in keys
    let normalized = decoded.replace('\\', "_");
    let base = normalized
        .split('/')
        .next_back()
        .unwrap_or(normalized.as_str())
        .to_string();
    if base.is_empty() || base == "." || base == ".." {
        return Err(anyhow!("invalid basename"));
    }
    Ok(base)
}

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

    const FIXTURE: &str = include_str!("../../tests/fixtures/pwc_al_publications_fragment.html");

    #[test]
    fn parse_fixture_has_tax_and_legal_links() {
        let links = parse_publication_links(FIXTURE);
        let urls: Vec<&str> = links.iter().map(|l| l.url.as_str()).collect();
        assert!(
            urls.iter().any(|u| u.contains("Tax_Alert_January_2025.pdf")),
            "missing tax pdf: {:?}",
            urls
        );
        assert!(
            urls.iter().any(|u| u.contains("Law_On_Cybersecurity.html")),
            "missing legal html: {:?}",
            urls
        );
        assert!(
            urls.iter().any(|u| u.ends_with("Legal_Alert_Albania_June2024.pdf")),
            "missing absolute pdf"
        );
    }

    #[test]
    fn dedupes_duplicate_href() {
        let html = r#"<div class="secnav-twocol__listing serif">
            <a class="strip-btn" href="https://www.pwc.com/al/en/same.pdf">
                <span class="strip-btn__text">First</span>
            </a>
            <a class="strip-btn" href="https://www.pwc.com/al/en/same.pdf">
                <span class="strip-btn__text">Second</span>
            </a>
        </div>"#;
        let links = parse_publication_links(html);
        assert_eq!(links.len(), 1);
        assert_eq!(links[0].title, "First");
    }

    #[test]
    fn normalize_relative_href() {
        let u = normalize_publication_href("/al/en/foo.pdf").unwrap();
        assert_eq!(u, "https://www.pwc.com/al/en/foo.pdf");
    }

    #[test]
    fn rejects_non_pwc_host() {
        assert!(normalize_publication_href("https://evil.com/x.pdf").is_err());
    }

    #[test]
    fn classify_pdf_html() {
        assert_eq!(
            classify_asset("/x/Y.pdf"),
            Some(AssetKind::Pdf)
        );
        assert_eq!(
            classify_asset("/path/doc.HTML"),
            Some(AssetKind::Html)
        );
        assert_eq!(classify_asset("/x/doc"), None);
    }
}
