use feed_rs::model::Entry;
use feed_rs::parser;
use futures::future::join_all;
use jsonwebtoken::{encode, Algorithm, EncodingKey, Header};
use maud::html;
use reqwest::Client;
use scraper::{Html, Selector};
use serde::{Deserialize, Serialize};
use std::env;

#[derive(Debug, Serialize, Deserialize)]
struct Claims {
    iat: usize,
    exp: usize,
    aud: String,
}

#[derive(Serialize, Debug)]
struct PostPayload {
    posts: Vec<Post>,
}

#[derive(Serialize, Debug, Clone)]
struct Post {
    title: String,
    slug: String,
    html: String,
    status: String,
    published_at: String,
    updated_at: String,
    canonical_url: String,
    tags: Vec<String>,
    feature_image: Option<String>,
    feature_image_alt: Option<String>,
    feature_image_caption: Option<String>,
    meta_description: Option<String>,
    custom_excerpt: Option<String>,
}

impl Post {
    async fn new(entry: Entry) -> Post {
        let title = entry.title.as_ref().unwrap().content.clone();

        let link = entry.links.first().unwrap().href.as_str();
        let slug = get_slug(link);

        let summary = summarize_url(link).await;
        
        // Extract content from ghost-optimized version
        let ghost_content = extract_article_content(&link).await;
        
        let html = html! {
            div class="ghost-summary" {
                h3 { "Summary" }
                p { (summary) }
            }
            div class="ghost-content" {
                (maud::PreEscaped(ghost_content))
            }
            div class="ghost-footer" {
                hr {}
                p {
                    em {
                        "This content was originally posted on my projects website " 
                        a href=(link) { "here" }
                        ". The above summary was generated by the " 
                        a href=("https://help.kagi.com/kagi/api/summarizer.html") {"Kagi Summarizer"} 
                        "."
                    }
                }
            }
        }.into_string();

        let status = "published".to_owned();

        let published_at = entry.published.unwrap().to_rfc3339();

        let updated_at = chrono::Utc::now().to_rfc3339();

        let canonical_url = link.to_owned();

        let mut tags: Vec<String> = entry
            .categories
            .iter()
            .map(|category| category.term.as_str().to_owned())
            .collect();
        tags.push("Projects Website".to_owned());

        // The rest of the data is optional and has to be pulled from the documents OpenGraph
        let raw_html = reqwest::get(link).await.unwrap().text().await.unwrap();
        let document = Html::parse_document(&raw_html);
        let selector = Selector::parse("meta[property^='og:']").unwrap();

        let mut feature_image = None;
        let mut feature_image_alt = None;
        let mut feature_image_caption = None;
        let mut meta_description = None;
        let mut custom_excerpt = None;

        for meta in document.select(&selector) {
            match meta.value().attr("property") {
                Some("og:image") => feature_image = meta.value().attr("content").map(String::from),
                Some("og:image:alt") => {
                    // Ghost API limits this to 190 chars
                    feature_image_alt = meta.value().attr("content").map(|desc| {
                        desc.chars().take(190).collect()
                    })
                }
                Some("og:image:description") => {
                    feature_image_caption = meta.value().attr("content").map(String::from)
                }
                Some("og:description") => {
                    meta_description = meta.value().attr("content").map(String::from);

                    // Ghost API limits this to 300 chars
                    custom_excerpt = meta.value().attr("content").map(|desc| {
                        desc.chars().take(300).collect()
                    });
                }
                _ => {}
            }
        }
        

        let x = Post {
            title,
            slug,
            html,
            status,
            published_at,
            updated_at,
            canonical_url,
            tags,
            feature_image,
            feature_image_alt,
            feature_image_caption,
            meta_description,
            custom_excerpt,
        };
        dbg!(&x);
        x
    }
}

fn get_slug(link: &str) -> String {
    link.split_once("/posts/").unwrap().1.to_string()
}

async fn extract_article_content(original_link: &str) -> String {
    // Convert original link to ghost-content version
    let ghost_link = original_link.replace("projects.ansonbiggs.com", "projects.ansonbiggs.com/ghost-content");
    
    match reqwest::get(&ghost_link).await {
        Ok(response) => {
            match response.text().await {
                Ok(html_content) => {
                    let document = Html::parse_document(&html_content);
                    
                    // Try different selectors to find the main content
                    let content_selectors = [
                        "#quarto-content main",
                        "#quarto-content",
                        "main",
                        "article",
                        ".content",
                        "body"
                    ];
                    
                    for selector_str in &content_selectors {
                        if let Ok(selector) = Selector::parse(selector_str) {
                            if let Some(element) = document.select(&selector).next() {
                                let content = element.inner_html();
                                
                                if !content.trim().is_empty() {
                                    return content;
                                }
                            }
                        }
                    }
                    
                    // Fallback: return original content with iframe if extraction fails
                    format!(r#"<div class="fallback-iframe">
                        <p><em>Content extraction failed. Falling back to embedded view:</em></p>
                        <iframe src="{}" style="width: 100%; height: 80vh; border: none;" loading="lazy"></iframe>
                    </div>"#, original_link)
                }
                Err(_) => format!(r#"<p><em>Failed to fetch content. <a href="{}">View original post</a></em></p>"#, original_link)
            }
        }
        Err(_) => format!(r#"<p><em>Failed to fetch content. <a href="{}">View original post</a></em></p>"#, original_link)
    }
}

async fn check_if_post_exists(entry: &Entry) -> bool {
    let posts_url = "https://notes.ansonbiggs.com/";
    let link = entry.links.first().unwrap().href.as_str();
    let slug = get_slug(link);

    match reqwest::get(format!("{}{}", posts_url, slug)).await {
        Ok(response) => response.status().is_success(),
        Err(_) => false,
    }
}

#[derive(Deserialize, Debug)]
struct GhostPostsResponse {
    posts: Vec<GhostPost>,
}

#[derive(Deserialize, Debug)]
struct GhostPost {
    id: String,
    slug: String,
}

async fn get_existing_post_id(slug: &str, token: &str) -> Option<String> {
    let client = Client::new();
    let api_url = format!("https://notes.ansonbiggs.com/ghost/api/v3/admin/posts/slug/{}/", slug);
    
    match client
        .get(&api_url)
        .header("Authorization", format!("Ghost {}", token))
        .send()
        .await
    {
        Ok(response) => {
            if response.status().is_success() {
                if let Ok(ghost_response) = response.json::<GhostPostsResponse>().await {
                    ghost_response.posts.first().map(|post| post.id.clone())
                } else {
                    None
                }
            } else {
                None
            }
        }
        Err(_) => None,
    }
}

async fn fetch_feed(url: &str) -> Vec<Entry> {
    let content = reqwest::get(url).await.unwrap().text().await.unwrap();

    let feed = parser::parse(content.as_bytes()).unwrap();

    feed.entries
}

#[derive(Serialize)]
struct KagiPayload {
    url: String,
    engine: String,
    summary_type: String,
    target_language: String,
    cache: bool,
}
impl KagiPayload {
    fn new(url: &str) -> Self {
        // https://help.kagi.com/kagi/api/summarizer.html
        KagiPayload {
            url: url.to_string(),
            engine: "agnes".to_string(), // Formal, technical, analytical summary
            summary_type: "summary".to_string(),
            target_language: "EN".to_string(),
            cache: true,
        }
    }
}

#[derive(Deserialize)]
struct KagiResponse {
    data: Data,
}

#[derive(Deserialize)]
struct Data {
    output: String,
}

async fn summarize_url(url: &str) -> String {
    let kagi_url = "https://kagi.com/api/v0/summarize";
    let kagi_api_key = env::var("kagi_api_key").unwrap();

    let payload = KagiPayload::new(url);

    let client = Client::new();

    let response = client
        .post(kagi_url)
        .header("Authorization", format!("Bot {}", kagi_api_key))
        .json(&payload)
        .send()
        .await
        .expect("Kagi request failed");

    response
        .json::<KagiResponse>()
        .await
        .expect("Kagi didn't return json")
        .data
        .output
}
#[tokio::main]
async fn main() {
    let ghost_api_url = "https://notes.ansonbiggs.com/ghost/api/v3/admin/posts/?source=html";
    let ghost_admin_api_key = env::var("admin_api_key").unwrap();

    // Check if force update is enabled
    let force_update = env::var("FORCE_UPDATE").unwrap_or_default() == "true";
    
    if force_update {
        println!("🔄 FORCE UPDATE MODE ENABLED");
        println!("   This will update ALL posts, including existing ones.");
    } else {
        println!("📝 NORMAL MODE - Only publishing new posts");
    }

    let feed = "https://projects.ansonbiggs.com/index.xml";

    // Split the key into ID and SECRET
    let (id, secret) = ghost_admin_api_key
        .split_once(':')
        .expect("Invalid API key format");

    // Prepare JWT header and claims
    let iat = chrono::Utc::now().timestamp() as usize;
    let exp = iat + 5 * 60;
    let claims = Claims {
        iat,
        exp,
        aud: "v3/admin".into(),
    };

    let header = Header {
        alg: Algorithm::HS256,
        kid: Some(id.into()),
        ..Default::default()
    };

    // Encode the JWT
    let token = encode(
        &header,
        &claims,
        &EncodingKey::from_secret(&hex::decode(secret).expect("Invalid secret hex")),
    )
    .expect("JWT encoding failed");

    // Prepare the post data
    let entries = fetch_feed(feed).await;

    let filtered_entries: Vec<Entry> = if force_update {
        println!("🔄 Force update enabled - processing all {} posts", entries.len());
        entries
    } else {
        let post_exists_futures = entries.into_iter().map(|entry| {
            let entry_clone = entry.clone();
            async move { (entry_clone, check_if_post_exists(&entry).await) }
        });

        let post_exists_results = join_all(post_exists_futures).await;

        let new_entries: Vec<Entry> = post_exists_results
            .into_iter()
            .filter_map(|(entry, exists)| if !exists { Some(entry) } else { None })
            .collect();

        println!("📝 Found {} new posts to publish", new_entries.len());
        new_entries
    };

    if filtered_entries.is_empty() {
        println!("Nothing to post.");
        return;
    }

    let post_futures = filtered_entries.into_iter().map(Post::new);

    let client = Client::new();

    for post in join_all(post_futures).await {
        let post_payload = PostPayload {
            posts: vec![post.clone()],
        };

        // Check if this is an update (for force_update mode)
        let (method, url) = if force_update {
            if let Some(existing_id) = get_existing_post_id(&post.slug, &token).await {
                println!("🔄 Updating existing post: {}", post.title);
                ("PUT", format!("https://notes.ansonbiggs.com/ghost/api/v3/admin/posts/{}/", existing_id))
            } else {
                println!("📝 Creating new post: {}", post.title);
                ("POST", ghost_api_url.to_string())
            }
        } else {
            println!("📝 Creating new post: {}", post.title);
            ("POST", ghost_api_url.to_string())
        };

        let response = match method {
            "PUT" => client
                .put(&url)
                .header("Authorization", format!("Ghost {}", token))
                .json(&post_payload)
                .send()
                .await
                .expect("Request failed"),
            _ => client
                .post(&url)
                .header("Authorization", format!("Ghost {}", token))
                .json(&post_payload)
                .send()
                .await
                .expect("Request failed"),
        };

        // Check the response
        if response.status().is_success() {
            let action = if method == "PUT" { "updated" } else { "published" };
            println!("✅ Post '{}' {} successfully.", post.title, action);
        } else {
            let action = if method == "PUT" { "update" } else { "publish" };
            println!(
                "❌ Failed to {} post '{}'.\n\tStatus: {}\n\tResponse: {:?}",
                action, &post.title, response.status(), response.text().await.unwrap_or_default()
            );
        }
    }
}
